diff --git a/.htmltest.yml b/.htmltest.yml
index 4e9cba8b44e8..3694384ababd 100644
--- a/.htmltest.yml
+++ b/.htmltest.yml
@@ -82,6 +82,9 @@ IgnoreURLs: # list of regexes of paths or URLs to be ignored
- ^https://github\.com/open-telemetry/opentelemetry.io/tree/main/content/en/[^d]
- ^https://github\.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/[^s]
- ^https://github\.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/security
+ # TODO: remove once https://github.com/open-telemetry/opentelemetry.io/pull/9850 is merged;
+ # this file is new and won't exist on main until then:
+ - ^https://github\.com/open-telemetry/opentelemetry\.io/tree/main/content/en/docs/specs/otel-config/
# FIXME: same issue as for the OTel spec mentioned above:
- ^https://github.com/open-telemetry/semantic-conventions/tree/main
diff --git a/assets/js/configTypesAccordion.js b/assets/js/configTypesAccordion.js
new file mode 100644
index 000000000000..0c21904c7e64
--- /dev/null
+++ b/assets/js/configTypesAccordion.js
@@ -0,0 +1,276 @@
+import {
+ expandAll,
+ collapseAll,
+ filterItems,
+ normalizeForSearch,
+ typeMatchesSearch,
+} from './shared/accordionUtils.js';
+
+const CONTAINER_SEL = '.config-types-accordion';
+const ACCORDION_ID = 'ct-accordion';
+const NO_RESULTS_ID = 'ct-no-results';
+const COUNT_ID = 'ct-count';
+
+// ── i18n ──────────────────────────────────────────────────────────────────────
+
+function readI18n(container) {
+ const d = container.dataset;
+ return {
+ search: d.i18nSearch,
+ filterAll: d.i18nFilterAll,
+ filterStable: d.i18nFilterStable,
+ filterExperimental: d.i18nFilterExperimental,
+ expandAll: d.i18nExpandAll,
+ collapseAll: d.i18nCollapseAll,
+ noResults: d.i18nNoResults,
+ loading: d.i18nLoading,
+ colType: d.i18nColType,
+ colConstraints: d.i18nColConstraints,
+ colDescription: d.i18nColDescription,
+ };
+}
+
+// ── Rendering ─────────────────────────────────────────────────────────────────
+
+function escapeAttr(str) {
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(//g, '>');
+}
+
+function escapeHtml(str) {
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+}
+
+function renderControls(types, i18n) {
+ const stableCount = types.filter((t) => !t.isExperimental).length;
+ const expCount = types.filter((t) => t.isExperimental).length;
+ const total = types.length;
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Showing ${total} of ${total} types
+
+
`;
+}
+
+function renderPropertiesTable(type, i18n) {
+ if (type.hasNoProperties) {
+ return `No configurable properties.
`;
+ }
+
+ const hasConstraints = type.properties.some((p) => p.constraints);
+
+ const rows = type.properties
+ .map(
+ (prop) => `
+
+ ${escapeHtml(prop.name)} |
+ ${escapeHtml(prop.type)} |
+ ${hasConstraints ? `${escapeHtml(prop.constraints)} | ` : ''}
+ |
+
`,
+ )
+ .join('');
+
+ return `
+
+
+
+ | Name |
+ ${escapeHtml(i18n.colType)} |
+ ${hasConstraints ? `${escapeHtml(i18n.colConstraints)} | ` : ''}
+ ${escapeHtml(i18n.colDescription)} |
+
+
+ ${rows}
+
`;
+}
+
+function renderTypeItem(type, i18n) {
+ const propCount = type.hasNoProperties ? 0 : (type.properties?.length ?? 0);
+ const countText = propCount === 1 ? '1 property' : `${propCount} properties`;
+ const constraintsHtml = type.constraints
+ ? `Type constraints: ${escapeHtml(type.constraints)}
`
+ : '';
+
+ return `
+
+
+
+
+ ${renderPropertiesTable(type, i18n)}
+ ${constraintsHtml}
+
+
+
`;
+}
+
+function renderAccordion(types, i18n) {
+ const items = types.map((t) => renderTypeItem(t, i18n)).join('');
+ return `
+
+ ${items}
+
+${escapeHtml(i18n.noResults)}
`;
+}
+
+// ── Description injection ─────────────────────────────────────────────────────
+
+// Descriptions are pre-rendered safe HTML (ul/ol/li/a only, per configSchemaTransform.mjs).
+// They are injected via innerHTML after the DOM is built to avoid double-escaping.
+function injectDescriptions(container, types) {
+ for (const type of types) {
+ for (const prop of type.properties ?? []) {
+ if (!prop.description) continue;
+ const cell = container.querySelector(
+ `[data-type-id="${CSS.escape(type.id)}"] [data-prop-desc="${CSS.escape(prop.name)}"]`,
+ );
+ if (cell) cell.innerHTML = prop.description;
+ }
+ }
+}
+
+// ── Filter / search ───────────────────────────────────────────────────────────
+
+function applyFilters(container, types, state) {
+ const accordion = container.querySelector(`#${ACCORDION_ID}`);
+ const countEl = container.querySelector(`#${COUNT_ID}`);
+ const noResults = container.querySelector(`#${NO_RESULTS_ID}`);
+
+ const q = normalizeForSearch(state.query);
+ let count = 0;
+
+ filterItems(accordion, (item) => {
+ const isExp = item.dataset.isExperimental === 'true';
+ const typeObj = types.find((t) => t.id === item.dataset.typeId);
+ const filterMatch =
+ state.filter === 'all' ||
+ (state.filter === 'stable' && !isExp) ||
+ (state.filter === 'experimental' && isExp);
+ const searchMatch = typeMatchesSearch(typeObj, q);
+ if (filterMatch && searchMatch) count++;
+ return filterMatch && searchMatch;
+ });
+
+ if (countEl)
+ countEl.textContent = `Showing ${count} of ${types.length} types`;
+ if (noResults) noResults.classList.toggle('d-none', count > 0);
+}
+
+// ── Event wiring ──────────────────────────────────────────────────────────────
+
+function wireControls(container, types) {
+ const accordion = container.querySelector(`#${ACCORDION_ID}`);
+ const state = { query: '', filter: 'all' };
+ let debounceTimer;
+
+ // Search
+ const searchInput = container.querySelector('#ct-search');
+ if (searchInput) {
+ searchInput.addEventListener('input', () => {
+ clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(() => {
+ state.query = searchInput.value;
+ applyFilters(container, types, state);
+ }, 300);
+ });
+ }
+
+ // Filter buttons
+ container.querySelectorAll('[data-ct-filter]').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ container
+ .querySelectorAll('[data-ct-filter]')
+ .forEach((b) => b.classList.remove('active'));
+ btn.classList.add('active');
+ state.filter = btn.dataset.ctFilter;
+ applyFilters(container, types, state);
+ });
+ });
+
+ // Expand / collapse all
+ const expandBtn = container.querySelector('#ct-expand-all');
+ const collapseBtn = container.querySelector('#ct-collapse-all');
+ if (expandBtn)
+ expandBtn.addEventListener('click', () => expandAll(accordion));
+ if (collapseBtn)
+ collapseBtn.addEventListener('click', () => collapseAll(accordion));
+}
+
+// ── Init ──────────────────────────────────────────────────────────────────────
+
+async function init() {
+ const container = document.querySelector(CONTAINER_SEL);
+ if (!container) return;
+
+ const schemaUrl = container.dataset.schemaUrl;
+ const i18n = readI18n(container);
+
+ container.innerHTML = `${escapeHtml(i18n.loading)}
`;
+
+ try {
+ const res = await fetch(schemaUrl);
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ const types = data.types;
+
+ container.innerHTML =
+ renderControls(types, i18n) + renderAccordion(types, i18n);
+ injectDescriptions(container, types);
+ wireControls(container, types);
+ } catch (err) {
+ container.innerHTML = `Failed to load configuration types.
`;
+ console.error('config-types-accordion:', err);
+ }
+}
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+} else {
+ init();
+}
diff --git a/assets/js/shared/accordionUtils.js b/assets/js/shared/accordionUtils.js
new file mode 100644
index 000000000000..12a74ce8e52d
--- /dev/null
+++ b/assets/js/shared/accordionUtils.js
@@ -0,0 +1,62 @@
+/**
+ * Shared utilities for config documentation accordions.
+ * Used by configTypesAccordion.js and configLangStatusAccordion.js.
+ */
+
+export function expandAll(container) {
+ container.querySelectorAll('.accordion-collapse').forEach((el) => {
+ el.classList.add('show');
+ const btn = el
+ .closest('.accordion-item')
+ ?.querySelector('.accordion-button');
+ if (btn) {
+ btn.classList.remove('collapsed');
+ btn.setAttribute('aria-expanded', 'true');
+ }
+ });
+}
+
+export function collapseAll(container) {
+ container.querySelectorAll('.accordion-collapse').forEach((el) => {
+ el.classList.remove('show');
+ const btn = el
+ .closest('.accordion-item')
+ ?.querySelector('.accordion-button');
+ if (btn) {
+ btn.classList.add('collapsed');
+ btn.setAttribute('aria-expanded', 'false');
+ }
+ });
+}
+
+/**
+ * Show or hide accordion items based on a predicate.
+ * @param {Element} container
+ * @param {function(Element): boolean} predicate
+ */
+export function filterItems(container, predicate) {
+ container.querySelectorAll('.accordion-item').forEach((item) => {
+ item.classList.toggle('d-none', !predicate(item));
+ });
+}
+
+export function normalizeForSearch(str) {
+ return str.toLowerCase().replace(/\s+/g, ' ').trim();
+}
+
+/**
+ * Check if a type object matches a search query.
+ * Searches type name, property names, and property types.
+ * Descriptions are not searched because they contain pre-rendered HTML.
+ * @param {{ name: string, properties?: Array<{name: string, type: string}> }} typeObj
+ * @param {string} normalizedQuery - result of normalizeForSearch()
+ */
+export function typeMatchesSearch(typeObj, normalizedQuery) {
+ if (!normalizedQuery) return true;
+ if (normalizeForSearch(typeObj.name).includes(normalizedQuery)) return true;
+ for (const prop of typeObj.properties ?? []) {
+ if (normalizeForSearch(prop.name).includes(normalizedQuery)) return true;
+ if (normalizeForSearch(prop.type).includes(normalizedQuery)) return true;
+ }
+ return false;
+}
diff --git a/assets/scss/_config_accordion_shared.scss b/assets/scss/_config_accordion_shared.scss
new file mode 100644
index 000000000000..22ea4667db66
--- /dev/null
+++ b/assets/scss/_config_accordion_shared.scss
@@ -0,0 +1,22 @@
+// Shared styles for config documentation accordions (config types, lang status).
+
+.config-types-controls {
+ background-color: var(--bs-tertiary-bg);
+ border: 1px solid var(--bs-border-color);
+ border-radius: var(--bs-border-radius);
+ padding: 0.75rem 1rem;
+}
+
+// Visual indicator for experimental types: left border accent.
+.accordion-item[data-is-experimental='true'] {
+ .accordion-button {
+ border-left: 3px solid var(--bs-warning);
+ }
+}
+
+// Keep the property count readable when the accordion button is in its open
+// (expanded) state — the button background darkens and `text-body-secondary`
+// would otherwise become hard to read.
+.accordion-button:not(.collapsed) .ct-prop-count {
+ opacity: 0.7;
+}
diff --git a/assets/scss/_config_types_accordion.scss b/assets/scss/_config_types_accordion.scss
new file mode 100644
index 000000000000..8cae6c834f0a
--- /dev/null
+++ b/assets/scss/_config_types_accordion.scss
@@ -0,0 +1,33 @@
+// Config types accordion — types-specific styles.
+
+.config-types-accordion {
+ .ct-props-table {
+ font-size: 0.875rem;
+
+ code {
+ // Prevent double-shrink from Bootstrap's inline code styling.
+ font-size: inherit;
+ // Allow long type names (e.g. ExperimentalComposableRuleBasedSampler) to wrap.
+ white-space: normal;
+ }
+
+ // Normalize lists inside pre-rendered description HTML.
+ td:last-child {
+ ul,
+ ol {
+ margin-bottom: 0;
+ padding-left: 1.25rem;
+ }
+ }
+ }
+
+ // Type-level constraints note at the bottom of each accordion body.
+ .ct-type-constraints {
+ font-size: 0.8125rem;
+ color: var(--bs-secondary-color);
+ border-top: 1px solid var(--bs-border-color-translucent);
+ padding-top: 0.5rem;
+ margin-top: 0.75rem;
+ margin-bottom: 0;
+ }
+}
diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss
index ba2fa1fdc2cd..5ca8d41c9ca6 100644
--- a/assets/scss/_styles_project.scss
+++ b/assets/scss/_styles_project.scss
@@ -18,6 +18,8 @@
@import 'java';
@import 'navbar';
@import 'training';
+@import 'config_accordion_shared';
+@import 'config_types_accordion';
.otel-docs-spec {
.td-page-meta__edit {
diff --git a/config/_default/module-template.yaml b/config/_default/module-template.yaml
index 28aec4876f61..12fd86408557 100644
--- a/config/_default/module-template.yaml
+++ b/config/_default/module-template.yaml
@@ -15,6 +15,8 @@ mounts:
# Specs, currently en only
- source: tmp/otel/specification
target: content/docs/specs/otel
+ - source: content/en/docs/specs/otel-config/types.md
+ target: content/docs/specs/otel/configuration/types.md
- source: tmp/opamp
target: content/docs/specs/opamp
- source: tmp/otlp/docs/specification.md
diff --git a/content/en/docs/specs/otel-config/types.md b/content/en/docs/specs/otel-config/types.md
new file mode 100644
index 000000000000..2a9df098ee3c
--- /dev/null
+++ b/content/en/docs/specs/otel-config/types.md
@@ -0,0 +1,27 @@
+---
+title: Configuration Types Reference
+linkTitle: Configuration Types
+description: >-
+ Searchable reference for all types defined in the OpenTelemetry declarative
+ configuration schema, including their properties and constraints.
+weight: 10
+# This file lives in the opentelemetry.io repo but is mounted into the
+# docs/specs/otel/ hierarchy, which cascades github_repo/github_subdir from
+# the opentelemetry-specification submodule. Override those params so the
+# Docsy "View page source" link points to the correct repo.
+github_repo: https://github.com/open-telemetry/opentelemetry.io
+github_subdir: ''
+path_base_for_github_subdir: ''
+---
+
+The OpenTelemetry
+[declarative configuration](/docs/specs/otel/configuration/data-model/) schema
+defines configuration types that describe the structure of SDK components
+configurable via a configuration file. Types prefixed with `Experimental` are
+subject to breaking changes without notice.
+
+For the full data model and schema, see
+[Data Model](/docs/specs/otel/configuration/data-model/). For SDK-specific
+usage, see [Configuration SDK](/docs/specs/otel/configuration/sdk/).
+
+{{< config-types-accordion >}}
diff --git a/i18n/en.yaml b/i18n/en.yaml
index a1d1fb184a8e..982f7dce52c1 100644
--- a/i18n/en.yaml
+++ b/i18n/en.yaml
@@ -42,3 +42,17 @@ collector_component_stability_footnote: >-
For details about component stability levels, see the [OpenTelemetry Collector
component stability
definitions](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md).
+
+# Config types accordion
+config_types_search_label: Search types and properties
+config_types_filter_all: All
+config_types_filter_stable: Stable
+config_types_filter_experimental: Experimental
+config_types_expand_all: Expand all
+config_types_collapse_all: Collapse all
+config_types_no_results: No types match your search.
+config_types_loading: Loading configuration types…
+config_types_col_name: Name
+config_types_col_type: Type
+config_types_col_constraints: Constraints
+config_types_col_description: Description
diff --git a/layouts/_shortcodes/config-types-accordion.html b/layouts/_shortcodes/config-types-accordion.html
new file mode 100644
index 000000000000..176f59e4568f
--- /dev/null
+++ b/layouts/_shortcodes/config-types-accordion.html
@@ -0,0 +1,18 @@
+{{- $schemaUrl := "/schemas/config-types.json" -}}
+
+
+
+
+{{ partial "script.html" (dict "src" "js/configTypesAccordion.js") -}}