Documentation
Header Renderer
Customize column headers with headerRenderer.
Add a header renderer
Set headerRenderer on a column to customize the header cell. Use header.label and other column fields from the props.
React TSX
Copy
const StatusHeader = ({ header }) => (<span style={{ fontWeight: 600 }}>{header.label}</span>);const columns: ReactColumnDef[] = [{ accessor: "status", label: "Status", width: 120, headerRenderer: StatusHeader },];
Angularstatus-header.component.ts
Copy
@Component({standalone: true,selector: "app-status-header",template: `<span style="font-weight:600">{{ header.label }}</span>`,})export class StatusHeaderComponent {@Input() header!: { label: string };}// column def{ accessor: "status", label: "Status", width: 120, headerRenderer: StatusHeaderComponent }
Vue SFC
Copy
import { h } from "vue";const StatusHeader = ({ header }) =>h("span", { style: { fontWeight: "600" } }, header.label);const columns: VueColumnDef[] = [{ accessor: "status", label: "Status", width: 120, headerRenderer: StatusHeader },];
Svelte
Copy
<!-- StatusHeader.svelte --><script lang="ts">import type { HeaderRendererProps } from "@simple-table/svelte";let { header }: HeaderRendererProps = $props();</script><span style="font-weight:600">{header.label}</span><!-- column def -->{ accessor: "status", label: "Status", width: 120, headerRenderer: StatusHeader }
Solid TSX
Copy
const StatusHeader = (props) => (<span style={{ "font-weight": "600" }}>{props.header.label}</span>);const columns: SolidColumnDef[] = [{ accessor: "status", label: "Status", width: 120, headerRenderer: StatusHeader },];
TypeScript
Copy
const StatusHeader = ({ header }) => {const span = document.createElement("span");span.style.fontWeight = "600";span.textContent = header.label;return span;};const columns: ColumnDef[] = [{ accessor: "status", label: "Status", width: 120, headerRenderer: StatusHeader },];
Reuse built-in components
Arrange components (labelContent, sortIcon, filterIcon, collapseIcon) so sort/filter keep working without reimplementing them.
React TSX
Copy
{accessor: "name",label: "Name",sortable: true,filterable: true,headerRenderer: ({ components }) => (<>{components?.labelContent}{components?.sortIcon}{components?.filterIcon}</>),}
Angularheader-layout.component.ts — receive @Input() components
Copy
// Template: place labelContent, sortIcon, filterIcon in your order{accessor: "name",label: "Name",sortable: true,filterable: true,headerRenderer: HeaderLayoutComponent,}
Vue SFC
Copy
import { h } from "vue";{accessor: "name",label: "Name",sortable: true,filterable: true,headerRenderer: ({ components }) =>h("div", { style: { display: "flex", gap: "4px", alignItems: "center" } }, [components?.labelContent,components?.sortIcon,components?.filterIcon,].filter(Boolean)),}
Svelte
Copy
<!-- HeaderLayout.svelte — place components.labelContent / sortIcon / filterIcon --><script lang="ts">import type { HeaderRendererProps } from "@simple-table/svelte";let { components }: HeaderRendererProps = $props();</script><!-- column def -->{ accessor: "name", label: "Name", sortable: true, filterable: true, headerRenderer: HeaderLayout }
Solid TSX
Copy
{accessor: "name",label: "Name",sortable: true,filterable: true,headerRenderer: (props) => (<>{props.components?.labelContent}{props.components?.sortIcon}{props.components?.filterIcon}</>),}
TypeScript
Copy
{accessor: "name",label: "Name",sortable: true,filterable: true,headerRenderer: ({ components }) => {const row = document.createElement("div");row.style.display = "flex";row.style.gap = "4px";row.style.alignItems = "center";if (components?.labelContent) {if (typeof components.labelContent === "string") {row.append(components.labelContent);} else {row.appendChild(components.labelContent);}}if (components?.sortIcon instanceof Node) row.appendChild(components.sortIcon);if (components?.filterIcon instanceof Node) row.appendChild(components.filterIcon);return row;},}
Hide all headers
To hide the header row entirely, set hideHeader on the table. See the API Reference.
Example
Custom header controls with built-in sort/filter icons. Use Code or StackBlitz for the full example.
React TSX
Copy
1import { useState } from "react";2import { SimpleTable } from "@simple-table/react";3import type { Theme, ReactColumnDef, HeaderRendererProps } from "@simple-table/react";4import { headerRendererConfig, type HeaderEmployee } from "./header-renderer.demo-data";5import "@simple-table/react/styles.css";67/**8 * Stateful header control. Pin should survive built-in sort/filter icon refreshes.9 * If core remounts the React headerRenderer on sort, the pin snaps back off.10 */11const StatefulHeader = ({ header, components }: HeaderRendererProps<HeaderEmployee>) => {12 const [pinned, setPinned] = useState(false);13 const [clicks, setClicks] = useState(0);1415 return (16 <div17 style={{18 display: "flex",19 alignItems: "center",20 gap: 6,21 width: "100%",22 minWidth: 0,23 padding: "0 4px"24 }}25 >26 <span27 style={{28 fontWeight: 600,29 overflow: "hidden",30 textOverflow: "ellipsis",31 whiteSpace: "nowrap",32 minWidth: 0,33 flex: 134 }}35 >36 {header.label}37 </span>38 <button39 type="button"40 aria-label={pinned ? `Unpin ${header.label}` : `Pin ${header.label}`}41 aria-pressed={pinned}42 title="Toggle pin, then sort — pin should stay on"43 onClick={(event) => {44 event.stopPropagation();45 setPinned((value) => !value);46 setClicks((n) => n + 1);47 }}48 style={{49 display: "inline-flex",50 alignItems: "center",51 justifyContent: "center",52 gap: 2,53 flexShrink: 0,54 height: 22,55 padding: "0 6px",56 border: "1px solid",57 borderColor: pinned ? "#6366f1" : "#d1d5db",58 borderRadius: 6,59 background: pinned ? "#eef2ff" : "#fff",60 color: pinned ? "#4f46e5" : "#6b7280",61 cursor: "pointer",62 fontSize: 12,63 lineHeight: 1,64 fontWeight: 60065 }}66 >67 {pinned ? "★" : "☆"} {clicks}68 </button>69 {components?.filterIcon}70 {components?.sortIcon}71 </div>72 );73};7475const headers: ReactColumnDef<HeaderEmployee>[] = headerRendererConfig.headers.map(76 (h): ReactColumnDef<HeaderEmployee> => ({77 ...h,78 // Keep built-in sorting so sort icon refresh goes through core's header path.79 sortable: h.sortable ?? true,80 filterable: h.accessor === "role" || h.accessor === "department",81 headerRenderer: StatefulHeader82}));8384const HeaderRendererDemo = ({85 height = "400px",86 theme87}: {88 height?: string | number;89 theme?: Theme;90}) => {91 return (92 <div style={{ display: "flex", flexDirection: "column", gap: 8, height: "100%" }}>93 <p style={{ margin: 0, fontSize: 13, color: "#6b7280", lineHeight: 1.4 }}>94 Click a header ★ to pin it, then sort that column. The pin (and click count) should stay —95 if the header remounts on sort, both reset.96 </p>97 <SimpleTable98 columns={headers}99 rows={headerRendererConfig.rows}100 height={height}101 theme={theme}102 columnResizing103 selectableCells104 getRowId={({ row }) => row.id}105 />106 </div>107 );108};109110export default HeaderRendererDemo;
Angularheader-demo-sort.ts
Copy
1import { signal } from "@angular/core";2import type { HeaderEmployee } from "./header-renderer.demo-data";34export type HeaderDemoSortDir = "asc" | "desc" | null;56type HeaderKey = keyof HeaderEmployee;78function isHeaderKey(accessor: string): accessor is HeaderKey {9 return (10 accessor === "id" ||11 accessor === "name" ||12 accessor === "email" ||13 accessor === "role" ||14 accessor === "salary" ||15 accessor === "department"16 );17}1819const CYCLE: HeaderDemoSortDir[] = ["asc", "desc", null];2021export const headerDemoSortAccessor = signal<HeaderKey | null>(null);22export const headerDemoSortDirection = signal<HeaderDemoSortDir>(null);2324export function cycleHeaderDemoSort(accessor: string): void {25 if (!isHeaderKey(accessor)) return;26 const currentAcc = headerDemoSortAccessor();27 const dir = headerDemoSortDirection();28 if (currentAcc !== accessor) {29 headerDemoSortAccessor.set(accessor);30 headerDemoSortDirection.set("asc");31 return;32 }33 const idx = CYCLE.indexOf(dir);34 const next = CYCLE[(idx + 1) % CYCLE.length]!;35 if (next) {36 headerDemoSortAccessor.set(accessor);37 headerDemoSortDirection.set(next);38 } else {39 headerDemoSortAccessor.set(null);40 headerDemoSortDirection.set(null);41 }42}434445// header-renderer-demo.component.ts46import { Component, computed, Input } from "@angular/core";47import { SimpleTableComponent } from "@simple-table/angular";48import type { AngularColumnDef, GetRowIdParams, Theme } from "@simple-table/angular";49import { headerDemoSortAccessor, headerDemoSortDirection } from "./header-demo-sort";50import { headerRendererConfig } from "./header-renderer.demo-data";51import type { HeaderEmployee } from "./header-renderer.demo-data";52import { HeaderSortableHeaderComponent } from "./header-sortable-header.component";53import "@simple-table/angular/styles.css";5455@Component({56 selector: "header-renderer-demo",57 standalone: true,58 imports: [SimpleTableComponent],59 template: `60 <simple-table61 [getRowId]="getRowId"62 [rows]="sortedData()"63 [columns]="headers()"64 [height]="height"65 [theme]="theme"66 ></simple-table>67 `,68})69export class HeaderRendererDemoComponent {70 @Input() height: string | number = "400px";71 @Input() theme?: Theme;7273 readonly sortedData = computed(() => {74 const acc = headerDemoSortAccessor();75 const dir = headerDemoSortDirection();76 if (!acc || !dir) return [...headerRendererConfig.rows];77 return [...headerRendererConfig.rows].sort((a, b) => {78 const aVal = a[acc];79 const bVal = b[acc];80 if (aVal === bVal) return 0;81 const cmp =82 typeof aVal === "number" && typeof bVal === "number"83 ? aVal - bVal84 : String(aVal).localeCompare(String(bVal));85 return dir === "asc" ? cmp : -cmp;86 });87 });8889 readonly headers = computed((): AngularColumnDef<HeaderEmployee>[] =>90 headerRendererConfig.headers.map((h) => ({91 ...h,92 sortable: false,93 headerRenderer: HeaderSortableHeaderComponent,94 })),95 );9697 getRowId = ({ row }: GetRowIdParams<HeaderEmployee>) => row.id;98}99100101// header-renderer.demo-data.ts102// Self-contained demo table setup for this example.103import type { AngularColumnDef } from "@simple-table/angular";104105export interface HeaderEmployee {106 id: number;107 name: string;108 email: string;109 role: string;110 salary: number;111 department: string;112}113114export const headerRendererData: HeaderEmployee[] = [115 { id: 1, name: "Alice Johnson", email: "alice@example.com", role: "Engineer", salary: 125000, department: "Engineering" },116 { id: 2, name: "Bob Martinez", email: "bob@example.com", role: "Designer", salary: 98000, department: "Design" },117 { id: 3, name: "Clara Chen", email: "clara@example.com", role: "PM", salary: 115000, department: "Product" },118 { id: 4, name: "David Kim", email: "david@example.com", role: "Engineer", salary: 132000, department: "Engineering" },119 { id: 5, name: "Elena Rossi", email: "elena@example.com", role: "Analyst", salary: 89000, department: "Analytics" },120 { id: 6, name: "Frank Müller", email: "frank@example.com", role: "Engineer", salary: 118000, department: "Engineering" },121 { id: 7, name: "Grace Park", email: "grace@example.com", role: "Designer", salary: 105000, department: "Design" },122 { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering" },123];124125export const headerRendererHeaders: AngularColumnDef<HeaderEmployee>[] = [126 { accessor: "id", label: "ID", width: 60, type: "number", sortable: true },127 { accessor: "name", label: "Employee Name", width: 180, type: "string", sortable: true },128 { accessor: "email", label: "Email Address", width: 200, type: "string" },129 { accessor: "role", label: "Job Role", width: 130, type: "string", sortable: true },130 { accessor: "salary", label: "Annual Salary", width: 140, type: "number", sortable: true },131 { accessor: "department", label: "Department", width: 150, type: "string", sortable: true },132];133134export const headerRendererConfig = {135 headers: headerRendererHeaders,136 rows: headerRendererData,137 tableProps: {138 selectableCells: true,139 columnResizing: true,140 },141};142143144// header-sortable-header.component.ts145import { Component, computed, Input, signal } from "@angular/core";146import type { HeaderRendererProps } from "@simple-table/angular";147import { cycleHeaderDemoSort, headerDemoSortAccessor, headerDemoSortDirection } from "./header-demo-sort";148149@Component({150 standalone: true,151 selector: "demo-header-sortable",152 template: `153 <div154 style="cursor:pointer;user-select:none;font-weight:600;display:flex;align-items:center;gap:4px;"155 (click)="onClick()"156 (keydown)="onKeydown($event)"157 role="button"158 tabindex="0"159 >160 <span>{{ header.label }}</span>161 @if (indicator()) {162 <span style="font-size:10px;color:#6366f1;">{{ indicator() }}</span>163 }164 </div>165 `,166})167export class HeaderSortableHeaderComponent {168 private readonly accessorStr = signal("");169170 @Input({ required: true }) set header(h: HeaderRendererProps["header"]) {171 this._header = h;172 this.accessorStr.set(String(h.accessor));173 }174 get header(): HeaderRendererProps["header"] {175 return this._header;176 }177 private _header!: HeaderRendererProps["header"];178179 @Input() accessor?: HeaderRendererProps["accessor"];180 @Input() colIndex?: HeaderRendererProps["colIndex"];181 @Input() components?: HeaderRendererProps["components"];182183 readonly indicator = computed(() => {184 headerDemoSortAccessor();185 headerDemoSortDirection();186 const acc = this.accessorStr();187 const isSorted = headerDemoSortAccessor() === acc;188 const dir = isSorted ? headerDemoSortDirection() : null;189 return dir === "asc" ? " ▲" : dir === "desc" ? " ▼" : "";190 });191192 onClick(): void {193 cycleHeaderDemoSort(this.accessorStr());194 }195196 onKeydown(e: KeyboardEvent): void {197 if (e.key === "Enter") this.onClick();198 }199}200
Vue SFC
Copy
1<script setup lang="ts">2import { ref, computed, defineComponent, h } from "vue";3import { SimpleTable } from "@simple-table/vue";4import type { Theme, VueColumnDef, GetRowIdParams } from "@simple-table/vue";5import { headerRendererConfig } from "./header-renderer.demo-data";6import type { HeaderEmployee } from "./header-renderer.demo-data";7import "@simple-table/vue/styles.css";89const props = withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {10 height: "400px",11});1213type SortDir = "asc" | "desc" | null;14const CYCLE: SortDir[] = ["asc", "desc", null];1516const sortAccessor = ref<string | null>(null);17const sortDirection = ref<SortDir>(null);1819const sortedData = computed(() => {20 if (!sortAccessor.value || !sortDirection.value) return [...headerRendererConfig.rows];21 const acc = sortAccessor.value;22 const dir = sortDirection.value;23 return [...headerRendererConfig.rows].sort((a, b) => {24 const aVal = a[acc as keyof HeaderEmployee];25 const bVal = b[acc as keyof HeaderEmployee];26 if (aVal === bVal) return 0;27 const cmp =28 typeof aVal === "number" && typeof bVal === "number"29 ? aVal - bVal30 : String(aVal).localeCompare(String(bVal));31 return dir === "asc" ? cmp : -cmp;32 });33});3435function makeSortableHeader(col: VueColumnDef<HeaderEmployee>) {36 return defineComponent({37 name: `SortHeader-${String(col.accessor)}`,38 setup() {39 return () => {40 const isSorted = sortAccessor.value === col.accessor;41 const dir = isSorted ? sortDirection.value : null;42 const indicator = dir === "asc" ? " ▲" : dir === "desc" ? " ▼" : "";43 const handleClick = () => {44 if (!isSorted) {45 sortAccessor.value = col.accessor as string;46 sortDirection.value = "asc";47 return;48 }49 const idx = CYCLE.indexOf(dir);50 const next = CYCLE[(idx + 1) % CYCLE.length];51 if (next) {52 sortAccessor.value = col.accessor as string;53 sortDirection.value = next;54 } else {55 sortAccessor.value = null;56 sortDirection.value = null;57 }58 };59 return h(60 "div",61 {62 onClick: handleClick,63 style: {64 cursor: "pointer",65 userSelect: "none",66 fontWeight: "600",67 display: "flex",68 alignItems: "center",69 gap: "4px",70 },71 },72 [73 h("span", col.label as string),74 indicator75 ? h("span", { style: { fontSize: "10px", color: "#6366f1" } }, indicator)76 : null,77 ],78 );79 };80 },81 });82}8384const headers = computed((): VueColumnDef<HeaderEmployee>[] =>85 headerRendererConfig.headers.map((col) => ({86 ...col,87 sortable: false,88 headerRenderer: makeSortableHeader(col),89 })),90);9192const getRowId = ({ row }: GetRowIdParams<HeaderEmployee>) => row.id;93</script>9495<template>96 <SimpleTable97 :columns="headers"98 :rows="sortedData"99 :get-row-id="getRowId"100 :height="props.height"101 :theme="props.theme"102 />103</template>
SvelteHeaderRendererDemo.svelte
Copy
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, SvelteColumnDef, GetRowIdParams } from "@simple-table/svelte";4 import { headerRendererConfig } from "./header-renderer.demo-data";5 import type { HeaderEmployee } from "./header-renderer.demo-data";6 import HeaderSortableHeader from "./HeaderSortableHeader.svelte";7 import { headerDemoSortAccessor, headerDemoSortDirection } from "./header-sort-store";8 import "@simple-table/svelte/styles.css";910 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();1112 const getRowId = ({ row }: GetRowIdParams<HeaderEmployee>) => row.id;1314 const sortedData = $derived.by(() => {15 const acc = $headerDemoSortAccessor as keyof HeaderEmployee | null;16 const dir = $headerDemoSortDirection;17 if (!acc || !dir) return [...headerRendererConfig.rows];18 return [...headerRendererConfig.rows].sort((a, b) => {19 const aVal = a[acc];20 const bVal = b[acc];21 if (aVal === bVal) return 0;22 const cmp =23 typeof aVal === "number" && typeof bVal === "number"24 ? aVal - bVal25 : String(aVal).localeCompare(String(bVal));26 return dir === "asc" ? cmp : -cmp;27 });28 });2930 const headers = $derived(31 headerRendererConfig.headers.map(32 (h): SvelteColumnDef<HeaderEmployee> => ({33 ...h,34 sortable: false,35 headerRenderer: HeaderSortableHeader,36 }),37 ),38 );39</script>4041<SimpleTable columns={headers} rows={sortedData} {getRowId} {height} {theme} />424344// HeaderSortableHeader.svelte45<script lang="ts">46 import type { HeaderRendererProps } from "@simple-table/svelte";47 import type { HeaderEmployee } from "./header-renderer.demo-data";48 import {49 headerDemoSortAccessor,50 headerDemoSortDirection,51 cycleHeaderDemoSort,52 } from "./header-sort-store";5354 let { header }: HeaderRendererProps<HeaderEmployee> = $props();55 const accessor = $derived(String(header.accessor));56 const isSorted = $derived($headerDemoSortAccessor === accessor);57 const dir = $derived(isSorted ? $headerDemoSortDirection : null);58 const indicator = $derived(dir === "asc" ? " ▲" : dir === "desc" ? " ▼" : "");59</script>6061<div62 style="cursor:pointer;user-select:none;font-weight:600;display:flex;align-items:center;gap:4px;"63 onclick={() => cycleHeaderDemoSort(accessor)}64 onkeydown={(e) => e.key === "Enter" && cycleHeaderDemoSort(accessor)}65 role="button"66 tabindex="0"67>68 <span>{header.label}</span>69 {#if indicator}70 <span style="font-size:10px;color:#6366f1;">{indicator}</span>71 {/if}72</div>73
Solid TSX
Copy
1import { createSignal, createMemo } from "solid-js";2import {SimpleTable} from "@simple-table/solid";import type { Theme, SolidColumnDef, HeaderRendererProps } from "@simple-table/solid";3import { headerRendererConfig, type HeaderEmployee } from "./header-renderer.demo-data";4import "@simple-table/solid/styles.css";56type SortDir = "asc" | "desc" | null;7const CycleOrder: SortDir[] = ["asc", "desc", null];89export default function HeaderRendererDemo(props: { height?: string | number; theme?: Theme }) {10 const [sortAccessor, setSortAccessor] = createSignal<string | null>(null);11 const [sortDirection, setSortDirection] = createSignal<SortDir>(null);1213 const sortedData = createMemo(() => {14 const acc = sortAccessor();15 const dir = sortDirection();16 if (!acc || !dir) return [...headerRendererConfig.rows];17 return [...headerRendererConfig.rows].sort((a, b) => {18 const key = acc as keyof HeaderEmployee;19 const aVal = a[key];20 const bVal = b[key];21 if (aVal === bVal) return 0;22 const cmp = typeof aVal === "number" && typeof bVal === "number"23 ? aVal - bVal24 : String(aVal).localeCompare(String(bVal));25 return dir === "asc" ? cmp : -cmp;26 });27 });2829 const headers = createMemo((): SolidColumnDef<HeaderEmployee>[] =>30 headerRendererConfig.headers.map((h) => ({31 ...h,32 sortable: false,33 headerRenderer: ({ accessor }: HeaderRendererProps<HeaderEmployee>) => {34 const isSorted = sortAccessor() === accessor;35 const dir = isSorted ? sortDirection() : null;36 const indicator = dir === "asc" ? " ▲" : dir === "desc" ? " ▼" : "";3738 const handleClick = () => {39 if (!isSorted) {40 setSortAccessor(accessor as string);41 setSortDirection("asc");42 return;43 }44 const idx = CycleOrder.indexOf(dir);45 const next = CycleOrder[(idx + 1) % CycleOrder.length];46 setSortAccessor(next ? (accessor as string) : null);47 setSortDirection(next);48 };4950 return (51 <div52 onClick={handleClick}53 style={{54 cursor: "pointer",55 "user-select": "none",56 "font-weight": "600",57 display: "flex",58 "align-items": "center",59 gap: "4px",60 }}61 >62 <span>{h.label}</span>63 {indicator && (64 <span style={{ "font-size": "10px", color: "#6366f1" }}>{indicator}</span>65 )}66 </div>67 );68 },69 }))70 );7172 return (73 <SimpleTable74 columns={headers()}75 getRowId={({ row }) => row.id}76 rows={sortedData()}77 height={props.height ?? "400px"}78 theme={props.theme}79 />80 );81}
TypeScriptHeaderRendererDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { HeaderEmployee } from "./header-renderer.demo-data";3import type { Theme, ColumnDef, GetRowIdParams } from "simple-table-core";4import { headerRendererConfig } from "./header-renderer.demo-data";5import "simple-table-core/styles.css";67type SortDir = "asc" | "desc" | null;8const CYCLE: SortDir[] = ["asc", "desc", null];910type HeaderKey = keyof HeaderEmployee;1112function isHeaderKey(accessor: string): accessor is HeaderKey {13 return (14 accessor === "id" ||15 accessor === "name" ||16 accessor === "email" ||17 accessor === "role" ||18 accessor === "salary" ||19 accessor === "department"20 );21}2223const getRowId = ({ row }: GetRowIdParams<HeaderEmployee>) => row.id;2425export function renderHeaderRendererDemo(26 container: HTMLElement,27 options?: { height?: string | number; theme?: Theme },28): SimpleTableVanilla<HeaderEmployee> {29 let sortAccessor: HeaderKey | null = null;30 let sortDirection: SortDir = null;3132 function getSortedData(): HeaderEmployee[] {33 if (!sortAccessor || !sortDirection) return [...headerRendererConfig.rows];34 const acc = sortAccessor;35 const dir = sortDirection;36 return [...headerRendererConfig.rows].sort((a, b) => {37 const aVal = a[acc];38 const bVal = b[acc];39 if (aVal === bVal) return 0;40 const cmp =41 typeof aVal === "number" && typeof bVal === "number"42 ? aVal - bVal43 : String(aVal).localeCompare(String(bVal));44 return dir === "asc" ? cmp : -cmp;45 });46 }4748 function buildHeaders(): ColumnDef<HeaderEmployee>[] {49 return headerRendererConfig.headers.map((h) => ({50 ...h,51 sortable: false,52 headerRenderer: () => {53 const isSorted = sortAccessor === h.accessor;54 const dir = isSorted ? sortDirection : null;55 const indicator = dir === "asc" ? " ▲" : dir === "desc" ? " ▼" : "";5657 const wrapper = document.createElement("div");58 Object.assign(wrapper.style, {59 cursor: "pointer",60 userSelect: "none",61 fontWeight: "600",62 display: "flex",63 alignItems: "center",64 gap: "4px",65 });66 wrapper.addEventListener("click", () => {67 if (!isHeaderKey(h.accessor)) return;6869 if (!isSorted) {70 sortAccessor = h.accessor;71 sortDirection = "asc";72 } else {73 const idx = CYCLE.indexOf(dir);74 const next = CYCLE[(idx + 1) % CYCLE.length];75 if (next) {76 sortAccessor = h.accessor;77 sortDirection = next;78 } else {79 sortAccessor = null;80 sortDirection = null;81 }82 }83 table.update({ columns: buildHeaders(), rows: getSortedData() });84 });8586 const label = document.createElement("span");87 label.textContent = h.label;88 wrapper.appendChild(label);8990 if (indicator) {91 const ind = document.createElement("span");92 Object.assign(ind.style, { fontSize: "10px", color: "#6366f1" });93 ind.textContent = indicator;94 wrapper.appendChild(ind);95 }9697 return wrapper;98 },99 }));100 }101102 const table = new SimpleTableVanilla(container, {103 getRowId,104 columns: buildHeaders(),105 rows: getSortedData(),106 height: options?.height ?? "400px",107 theme: options?.theme,108 });109110 return table;111}112113114// header-renderer.demo-data.ts115// Self-contained demo table setup for this example.116import type { ColumnDef } from "simple-table-core";117118export interface HeaderEmployee {119 id: number;120 name: string;121 email: string;122 role: string;123 salary: number;124 department: string;125}126127export const headerRendererData: HeaderEmployee[] = [128 { id: 1, name: "Alice Johnson", email: "alice@example.com", role: "Engineer", salary: 125000, department: "Engineering" },129 { id: 2, name: "Bob Martinez", email: "bob@example.com", role: "Designer", salary: 98000, department: "Design" },130 { id: 3, name: "Clara Chen", email: "clara@example.com", role: "PM", salary: 115000, department: "Product" },131 { id: 4, name: "David Kim", email: "david@example.com", role: "Engineer", salary: 132000, department: "Engineering" },132 { id: 5, name: "Elena Rossi", email: "elena@example.com", role: "Analyst", salary: 89000, department: "Analytics" },133 { id: 6, name: "Frank Müller", email: "frank@example.com", role: "Engineer", salary: 118000, department: "Engineering" },134 { id: 7, name: "Grace Park", email: "grace@example.com", role: "Designer", salary: 105000, department: "Design" },135 { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering" },136];137138export const headerRendererHeaders: ColumnDef<HeaderEmployee>[] = [139 { accessor: "id", label: "ID", width: 60, type: "number", sortable: true },140 { accessor: "name", label: "Employee Name", width: 180, type: "string", sortable: true },141 { accessor: "email", label: "Email Address", width: 200, type: "string" },142 { accessor: "role", label: "Job Role", width: 130, type: "string", sortable: true },143 { accessor: "salary", label: "Annual Salary", width: 140, type: "number", sortable: true },144 { accessor: "department", label: "Department", width: 150, type: "string", sortable: true },145];146147export const headerRendererConfig = {148 headers: headerRendererHeaders,149 rows: headerRendererData,150 tableProps: {151 selectableCells: true,152 columnResizing: true,153 },154};155
Props
Header Renderer Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
ColumnDef.headerRenderer | Optional | Custom header content. Framework adapters accept components or render functions; vanilla returns a string or DOM node. |
Renderer arguments
HeaderRendererProps
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
accessor | Required | The column accessor/key identifying which column this header belongs to | |
colIndexnumber | Required | The zero-based index of the column within the table | |
header | Required | The complete ColumnDef containing all configuration for this column including label, width, and other properties | |
componentsHeaderRendererComponents | Optional | Object containing pre-rendered header components (sortIcon, filterIcon, collapseIcon, labelContent) that can be positioned anywhere in your custom header renderer. This gives you complete control over the layout and order of header elements. |