Purpose#
The CustomTable component renders the records of a merchant-defined custom table inside any
Noir page.It is used to show structured data the platform has no built-in model for — store locators, FAQ
entries, care guides, size charts — either as a responsive card grid or as an accordion, with
pagination when the table holds more records than fit on one page.Records are fetched and shaped server-side; the component only renders them and drives the
accordion open/closed state.Model shape (storefront example)#
{
"name": "CustomTable",
"view": "Default",
"section": "SectionA",
"settings": {
"id": "Component Id",
"cssClass": "",
"header": "Sample title",
"layout": "List",
"alignment": "Left"
},
"records": [
{
"title": "Sample title",
"fields": [
{
"label": "Sample label",
"value": "Sample value"
}
]
}
],
"pagination": {
"pageNumber": 1,
"numberOfPages": 3,
"totalCount": 7,
"pageParameterName": "page"
},
"translations": {
"customTable": "Sample translation",
"untitled": "Sample translation",
"...": "..."
}
}
Required fields#
Optional fields#
settings.cssClass — appended to the root <section> when it is not empty and not (UNDEFINED).
settings.header — rendered as the component heading; also used as the section's aria-label.
settings.layout — List (default) or Accordion.
settings.alignment — Left (default), Center or Right; drives the header alignment classes.
records[].title — falls back to the Untitled translation when empty.
records[].fields[].label / records[].fields[].value
pagination.pageNumber (defaults to 1), pagination.numberOfPages, pagination.totalCount
pagination.pageParameterName — the query-string parameter the pager writes (defaults to page),
so two custom tables on the same page can page independently.
Template behavior (Liquid + Alpine)#
Source: Components/CustomTable/Default.liquid.Renders nothing at all when records is empty. The whole <section> is inside
{% if recordsCount > 0 %}, so an unpublished, unknown or temporarily unreachable table never
breaks the page.
Wraps content in the standard <section id="comp-{{ id }}" class="component customTable ...">,
marked role="region" and labelled with the header when one is configured.
x-data="customtabledefault.initComponent({{ pageParameterName | serialize | escape }})"
List layout (default): a responsive grid (1 / 2 / 3 columns) of bordered cards, each with the
record title and its label/value rows.
Accordion layout: one collapsible entry per record. The toggle button carries
aria-expanded + aria-controls, and the panel is x-shown with x-cloak so it does not flash
open before Alpine initializes.
Field rows are a 12-column grid that stacks on mobile; the label column is lg:col-span-3 in the
accordion and lg:col-span-4 in the list.
Computes a Liquid sliding-window pagination list: all pages when numberOfPages <= 7, otherwise a
window around the current page with ... gaps and the first/last page always reachable.
The pager is rendered only when numberOfPages > 1, inside a labelled <nav>. Previous/next and
the current page render as non-interactive <span aria-disabled="true"> / aria-current="page"
at the range ends.
JavaScript#
Global object#
Components/CustomTable/Default.js exposes:Liquid binds it via Alpine:<section x-data="customtabledefault.initComponent({{ pageParameterName | serialize | escape }})">
initComponent#
Creates the Alpine state for the component. Takes the query-string parameter name the pager should
write, defaulting to page, and holds openAccordionIndex.The open index is kept here rather than per item so that opening one accordion entry closes whichever
other one was open.isAccordionOpen#
Returns whether the accordion entry at the given index is the currently open one.toggleAccordion#
Opens the entry at the given index, or closes it when it is already open, by setting
openAccordionIndex to the index or null.redirectToState#
Navigates to the requested page by rewriting pageParameterName in the current query string and
reloading. Other query parameters are handed back untouched, so filters set elsewhere on the page
survive paging.Every destination is built from window.location.origin + window.location.pathname.
pathname alone is not a safe base: a page requested at https://shop.example//evil.example
reads back //evil.example, which the browser resolves as a protocol-relative URL to another host.
Notes#
Paging is a full page load, so every page is server-rendered and therefore indexable and linkable.
Deep paging gets progressively slower because the query re-scans from the start of the table.
Only the Noir theme has this component; Kitchenware has no equivalent.
Translations live in Components/CustomTable/Default.json (CustomTable, Untitled, Yes, No,
Pagination, PreviousPage, NextPage, GoToPageNumber, CurrentPageNumber).
For the merchant-facing feature documentation (visibility, quotas, storefront API), see
docs/features/custom-tables.md in the Ecom repository.
Modified at 2026-09-17 09:39:35