Documentation
Cell Renderer
Render custom cell UI — badges, links, progress bars, and other interactive content — with cellRenderer.
Add a cell renderer
Set cellRenderer on a column. Return text, or framework UI (components / DOM nodes). Prefer valueFormatter for plain text formatting.
React TSX
Copy
const StatusCell = ({ value }) => {const status = String(value);const color = status === "active" ? "#10B981" : "#6B7280";return <span style={{ color, fontWeight: 600 }}>{status}</span>;};const columns: ReactColumnDef[] = [{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell },];
Vue SFC
Copy
import { h } from "vue";const StatusCell = ({ value }) => {const status = String(value);const color = status === "active" ? "#10B981" : "#6B7280";return h("span", { style: { color, fontWeight: "600" } }, status);};const columns: VueColumnDef[] = [{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell },];
Angularstatus-cell.component.ts
Copy
@Component({standalone: true,selector: "app-status-cell",template: `<span [style.color]="color" style="font-weight:600">{{ status }}</span>`,})export class StatusCellComponent {@Input() value!: unknown;get status() { return String(this.value); }get color() { return this.status === "active" ? "#10B981" : "#6B7280"; }}// column def{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCellComponent }
Svelte
Copy
<!-- StatusCell.svelte --><script lang="ts">import type { CellRendererProps } from "@simple-table/svelte";let { value }: CellRendererProps = $props();const status = $derived(String(value));const color = $derived(status === "active" ? "#10B981" : "#6B7280");</script><span style="color:{color};font-weight:600">{status}</span><!-- column def -->{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell }
Solid TSX
Copy
const StatusCell = (props) => {const status = String(props.value);const color = status === "active" ? "#10B981" : "#6B7280";return <span style={{ color, "font-weight": "600" }}>{status}</span>;};const columns: SolidColumnDef[] = [{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell },];
TypeScript
Copy
const StatusCell = ({ value }) => {const status = String(value);const color = status === "active" ? "#10B981" : "#6B7280";const span = document.createElement("span");span.style.color = color;span.style.fontWeight = "600";span.textContent = status;return span;};const columns: ColumnDef[] = [{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell },];
Wrap formatted values
Pair with valueFormatter, then read formattedValue in the renderer when you only need custom chrome around already-formatted text.
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",cellRenderer: ({ formattedValue }) => (<strong>{formattedValue}</strong>),}
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",cellRenderer: ({ formattedValue }) => h("strong", String(formattedValue ?? "")),}
TypeScript
Copy
// Prefer formatting in the cell component, or pass formattedValue via @Input(){accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",cellRenderer: SalaryCellComponent, // @Input() formattedValue}
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",cellRenderer: SalaryCell, // receives formattedValue as a prop}
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: (props) =>typeof props.value === "number" ? `$${props.value.toLocaleString()}` : "",cellRenderer: (props) => <strong>{props.formattedValue}</strong>,}
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",cellRenderer: ({ formattedValue }) => {const el = document.createElement("strong");el.textContent = String(formattedValue ?? "");return el;},}
Example
Badges, links, progress, and more. Use Code or StackBlitz for the full example.
React TSX
Copy
1import { useMemo } from "react";2import { SimpleTable } from "@simple-table/react";3import type { Theme, CellRendererProps, ReactColumnDef } from "@simple-table/react";4import { cellRendererConfig } from "./cell-renderer.demo-data";5import type { CellRendererEmployee } from "./cell-renderer.demo-data";6import "@simple-table/react/styles.css";78const getInitials = (name: string) =>9 name10 .split(" ")11 .map((n) => n[0])12 .join("")13 .toUpperCase();1415const TeamCell = ({ row }: CellRendererProps) => {16 const members = (row as CellRendererEmployee).teamMembers;17 return (18 <div style={{ display: "flex", alignItems: "center", gap: 6 }}>19 {members.map((m) => (20 <div key={m.name} style={{ display: "flex", alignItems: "center", gap: 4 }}>21 <div22 style={{23 width: 24,24 height: 24,25 borderRadius: "50%",26 background: "#DBEAFE",27 color: "#1E40AF",28 display: "flex",29 alignItems: "center",30 justifyContent: "center",31 fontSize: 10,32 fontWeight: 600,33 flexShrink: 0,34 }}35 >36 {getInitials(m.name)}37 </div>38 <span style={{ fontSize: 13, whiteSpace: "nowrap" }}>{m.name}</span>39 </div>40 ))}41 </div>42 );43};4445const WebsiteCell = ({ value }: CellRendererProps) => {46 const url = String(value);47 return (48 <span>49 🌐{" "}50 <a51 href={`https://${url}`}52 target="_blank"53 rel="noopener noreferrer"54 style={{ color: "#2563EB", textDecoration: "none" }}55 onMouseEnter={(e) => (e.currentTarget.style.textDecoration = "underline")}56 onMouseLeave={(e) => (e.currentTarget.style.textDecoration = "none")}57 >58 {url}59 </a>60 </span>61 );62};6364const StatusCell = ({ value }: CellRendererProps) => {65 const status = String(value);66 const map: Record<string, { icon: string; color: string }> = {67 active: { icon: "✓", color: "#10B981" },68 inactive: { icon: "✕", color: "#EF4444" },69 pending: { icon: "!", color: "#F59E0B" },70 };71 const { icon, color } = map[status] ?? { icon: "?", color: "#6b7280" };72 return (73 <span style={{ color, fontWeight: 600, textTransform: "capitalize" }}>74 {icon} {status}75 </span>76 );77};7879const ProgressCell = ({ value }: CellRendererProps) => {80 const pct = Number(value) || 0;81 const color = pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981";82 return (83 <div>84 <div style={{ fontSize: 12, marginBottom: 2 }}>{pct}%</div>85 <div style={{ height: 10, background: "#E5E7EB", borderRadius: 5, overflow: "hidden" }}>86 <div87 style={{88 width: `${pct}%`,89 height: "100%",90 background: color,91 borderRadius: 5,92 transition: "width 0.3s",93 }}94 />95 </div>96 </div>97 );98};99100const RatingCell = ({ value }: CellRendererProps) => {101 const rating = Number(value) || 0;102 const full = Math.floor(rating);103 const hasHalf = rating % 1 >= 0.25;104 const empty = 5 - full - (hasHalf ? 1 : 0);105 return (106 <span style={{ display: "flex", alignItems: "center", gap: 4 }}>107 <span style={{ color: "#F59E0B", letterSpacing: 1 }}>108 {"★".repeat(full)}109 {hasHalf && <span style={{ opacity: 0.5 }}>★</span>}110 {"☆".repeat(Math.max(0, empty))}111 </span>112 <span style={{ fontSize: 12, color: "#6b7280" }}>{rating}</span>113 </span>114 );115};116117const VerifiedCell = ({ value }: CellRendererProps) => {118 const yes = Boolean(value);119 return (120 <span style={{ color: yes ? "#10B981" : "#EF4444", fontWeight: 600 }}>121 {yes ? "✓ Yes" : "✕ No"}122 </span>123 );124};125126const TagsCell = ({ value }: CellRendererProps) => {127 const raw = Array.isArray(value) ? value : [];128 const tags = raw.filter((t): t is string => typeof t === "string");129 return (130 <div style={{ display: "flex", gap: 4, flexWrap: "nowrap", overflow: "hidden" }}>131 {tags.map((tag) => (132 <span133 key={tag}134 style={{135 display: "inline-block",136 padding: "2px 8px",137 borderRadius: 4,138 fontSize: 12,139 fontWeight: 500,140 background: "#DBEAFE",141 color: "#1E40AF",142 whiteSpace: "nowrap",143 }}144 >145 {tag}146 </span>147 ))}148 </div>149 );150};151152const CellRendererDemo = ({153 height = "400px",154 theme,155}: {156 height?: string | number;157 theme?: Theme;158}) => {159 const headers = useMemo(160 () =>161 cellRendererConfig.headers.map((h) => {162 const renderers: Record<string, React.ComponentType<CellRendererProps>> = {163 teamMembers: TeamCell,164 website: WebsiteCell,165 status: StatusCell,166 progress: ProgressCell,167 rating: RatingCell,168 verified: VerifiedCell,169 tags: TagsCell,170 };171 const cellRenderer = renderers[h.accessor as string];172 return cellRenderer ? { ...h, cellRenderer } : h;173 }) as ReactColumnDef[],174 [],175 );176177 return (178 <SimpleTable179 columns={headers}180 rows={cellRendererConfig.rows}181 height={height}182 theme={theme}183 selectableCells={cellRendererConfig.tableProps.selectableCells}184 customTheme={cellRendererConfig.tableProps.customTheme}185 />186 );187};188189export default CellRendererDemo;
Vue SFC
Copy
1<script setup lang="ts">2import { h, computed } from "vue";3import { SimpleTable } from "@simple-table/vue";4import type { Theme, VueColumnDef, CellRendererProps } from "@simple-table/vue";5import { cellRendererConfig } from "./cell-renderer.demo-data";6import type { CellRendererEmployee } from "./cell-renderer.demo-data";7import "@simple-table/vue/styles.css";89withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {10 height: "400px",11});1213const getInitials = (name: string) =>14 name15 .split(" ")16 .map((n) => n[0])17 .join("")18 .toUpperCase();1920const TeamCell = ({ row }: CellRendererProps) => {21 const members = (row as CellRendererEmployee).teamMembers;22 return h(23 "div",24 { style: { display: "flex", alignItems: "center", gap: "6px" } },25 members.map((m) =>26 h("div", { key: m.name, style: { display: "flex", alignItems: "center", gap: "4px" } }, [27 h(28 "div",29 {30 style: {31 width: "24px",32 height: "24px",33 borderRadius: "50%",34 background: "#DBEAFE",35 color: "#1E40AF",36 display: "flex",37 alignItems: "center",38 justifyContent: "center",39 fontSize: "10px",40 fontWeight: "600",41 flexShrink: "0",42 },43 },44 getInitials(m.name),45 ),46 h("span", { style: { fontSize: "13px", whiteSpace: "nowrap" } }, m.name),47 ]),48 ),49 );50};5152const WebsiteCell = ({ value }: CellRendererProps) => {53 const url = String(value);54 return h("span", [55 "🌐 ",56 h(57 "a",58 {59 href: `https://${url}`,60 target: "_blank",61 rel: "noopener noreferrer",62 style: { color: "#2563EB", textDecoration: "none" },63 onMouseenter: (e: MouseEvent) => {64 (e.currentTarget as HTMLAnchorElement).style.textDecoration = "underline";65 },66 onMouseleave: (e: MouseEvent) => {67 (e.currentTarget as HTMLAnchorElement).style.textDecoration = "none";68 },69 },70 url,71 ),72 ]);73};7475const StatusCell = ({ value }: CellRendererProps) => {76 const status = String(value);77 const map: Record<string, { icon: string; color: string }> = {78 active: { icon: "✓", color: "#10B981" },79 inactive: { icon: "✕", color: "#EF4444" },80 pending: { icon: "!", color: "#F59E0B" },81 };82 const { icon, color } = map[status] ?? { icon: "?", color: "#6b7280" };83 return h(84 "span",85 { style: { color, fontWeight: "600", textTransform: "capitalize" } },86 `${icon} ${status}`,87 );88};8990const ProgressCell = ({ value }: CellRendererProps) => {91 const pct = Number(value) || 0;92 const color = pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981";93 return h("div", [94 h("div", { style: { fontSize: "12px", marginBottom: "2px" } }, `${pct}%`),95 h(96 "div",97 {98 style: {99 height: "10px",100 background: "#E5E7EB",101 borderRadius: "5px",102 overflow: "hidden",103 },104 },105 [106 h("div", {107 style: {108 width: `${pct}%`,109 height: "100%",110 background: color,111 borderRadius: "5px",112 transition: "width 0.3s",113 },114 }),115 ],116 ),117 ]);118};119120const RatingCell = ({ value }: CellRendererProps) => {121 const rating = Number(value) || 0;122 const full = Math.floor(rating);123 const hasHalf = rating % 1 >= 0.25;124 const empty = 5 - full - (hasHalf ? 1 : 0);125 return h("span", { style: { display: "flex", alignItems: "center", gap: "4px" } }, [126 h(127 "span",128 { style: { color: "#F59E0B", letterSpacing: "1px" } },129 [130 "★".repeat(full),131 hasHalf ? h("span", { style: { opacity: 0.5 } }, "★") : null,132 "☆".repeat(Math.max(0, empty)),133 ],134 ),135 h("span", { style: { fontSize: "12px", color: "#6b7280" } }, String(rating)),136 ]);137};138139const VerifiedCell = ({ value }: CellRendererProps) => {140 const yes = Boolean(value);141 return h(142 "span",143 { style: { color: yes ? "#10B981" : "#EF4444", fontWeight: "600" } },144 yes ? "✓ Yes" : "✕ No",145 );146};147148const TagsCell = ({ value }: CellRendererProps) => {149 const raw = Array.isArray(value) ? value : [];150 const tags = raw.filter((t): t is string => typeof t === "string");151 return h(152 "div",153 { style: { display: "flex", gap: "4px", flexWrap: "nowrap", overflow: "hidden" } },154 tags.map((tag) =>155 h(156 "span",157 {158 key: tag,159 style: {160 display: "inline-block",161 padding: "2px 8px",162 borderRadius: "4px",163 fontSize: "12px",164 fontWeight: "500",165 background: "#DBEAFE",166 color: "#1E40AF",167 whiteSpace: "nowrap",168 },169 },170 tag,171 ),172 ),173 );174};175176const RENDERERS: Record<string, (p: CellRendererProps) => ReturnType<typeof h>> = {177 teamMembers: TeamCell,178 website: WebsiteCell,179 status: StatusCell,180 progress: ProgressCell,181 rating: RatingCell,182 verified: VerifiedCell,183 tags: TagsCell,184};185186const headers = computed(() =>187 cellRendererConfig.headers.map((col) => {188 const r = RENDERERS[col.accessor as string];189 return r ? { ...col, cellRenderer: r } : { ...col };190 }),191);192</script>193194<template>195 <SimpleTable196 :columns="headers"197 :rows="cellRendererConfig.rows"198 :height="height"199 :theme="theme"200 :selectable-cells="cellRendererConfig.tableProps.selectableCells"201 :custom-theme="cellRendererConfig.tableProps.customTheme"202 />203</template>
Angularcell-renderer-demo.component.ts
Copy
1import { Component, Input } from "@angular/core";2import { SimpleTableComponent } from "@simple-table/angular";3import type { AngularCellRenderer, AngularColumnDef, Row, Theme } from "@simple-table/angular";4import { cellRendererConfig } from "./cell-renderer.demo-data";5import { CrProgressCellComponent } from "./cr-progress-cell.component";6import { CrRatingCellComponent } from "./cr-rating-cell.component";7import { CrStatusCellComponent } from "./cr-status-cell.component";8import { CrTagsCellComponent } from "./cr-tags-cell.component";9import { CrTeamMembersCellComponent } from "./cr-team-members-cell.component";10import { CrVerifiedCellComponent } from "./cr-verified-cell.component";11import { CrWebsiteCellComponent } from "./cr-website-cell.component";12import "@simple-table/angular/styles.css";1314const RENDERERS: Partial<Record<string, AngularCellRenderer>> = {15 teamMembers: CrTeamMembersCellComponent,16 website: CrWebsiteCellComponent,17 status: CrStatusCellComponent,18 progress: CrProgressCellComponent,19 rating: CrRatingCellComponent,20 verified: CrVerifiedCellComponent,21 tags: CrTagsCellComponent,22};2324@Component({25 selector: "cell-renderer-demo",26 standalone: true,27 imports: [SimpleTableComponent],28 template: `29 <simple-table30 [rows]="rows"31 [columns]="headers"32 [height]="height"33 [theme]="theme"34 [selectableCells]="true"35 [customTheme]="{ rowHeight: 48 }"36 ></simple-table>37 `,38})39export class CellRendererDemoComponent {40 @Input() height: string | number = "400px";41 @Input() theme?: Theme;4243 readonly rows: Row[] = cellRendererConfig.rows;44 readonly headers: AngularColumnDef[] = cellRendererConfig.headers.map((h): AngularColumnDef => {45 const cellRenderer = RENDERERS[String(h.accessor)];46 return cellRenderer ? { ...h, cellRenderer } : { ...h };47 });48}495051// cell-renderer.demo-data.ts52// Self-contained demo table setup for this example.53import type { AngularColumnDef } from "@simple-table/angular";545556export type CellRendererEmployee = {57 id: number;58 name: string;59 website: string;60 status: string;61 progress: number;62 rating: number;63 verified: boolean;64 tags: string[];65 teamMembers: { name: string; role: string }[];66};6768export const cellRendererData: CellRendererEmployee[] = [69 { id: 1, name: "Isabella Romano", website: "isabellaromano.design", status: "active", progress: 92, rating: 4.9, verified: true, tags: ["UI/UX", "Design", "Frontend"], teamMembers: [{ name: "Alice Smith", role: "Designer" }, { name: "Bob Johnson", role: "Developer" }] },70 { id: 2, name: "Ethan McKenzie", website: "ethanmckenzie.dev", status: "active", progress: 87, rating: 4.7, verified: true, tags: ["Web Development", "Backend", "API"], teamMembers: [{ name: "Charlie Brown", role: "Backend Developer" }, { name: "Diana Prince", role: "Frontend Developer" }] },71 { id: 3, name: "Zoe Patterson", website: "zoepatterson.com", status: "pending", progress: 34, rating: 4.2, verified: false, tags: ["Branding", "Marketing"], teamMembers: [{ name: "Eve Adams", role: "Marketing Manager" }] },72 { id: 4, name: "Felix Chang", website: "felixchang.mobile", status: "active", progress: 95, rating: 4.8, verified: true, tags: ["Mobile App", "UX/UI"], teamMembers: [{ name: "Grace Lee", role: "UX Designer" }, { name: "Hank Johnson", role: "Mobile Developer" }] },73 { id: 5, name: "Aria Gonzalez", website: "ariagonzalez.writer", status: "active", progress: 78, rating: 4.6, verified: true, tags: ["Content Writing", "Copywriting"], teamMembers: [{ name: "Ivy White", role: "Content Strategist" }] },74 { id: 6, name: "Jasper Flynn", website: "jasperflynn.tech", status: "inactive", progress: 12, rating: 3.8, verified: false, tags: ["Consulting", "Tech Strategy"], teamMembers: [{ name: "Kate Brown", role: "Consultant" }] },75 { id: 7, name: "Nova Sterling", website: "novasterling.marketing", status: "active", progress: 83, rating: 4.5, verified: true, tags: ["Digital Marketing", "SEO"], teamMembers: [{ name: "Leo Wilson", role: "SEO Specialist" }, { name: "Mia Davis", role: "Marketing Analyst" }] },76 { id: 8, name: "Cruz Martinez", website: "cruzmartinez.photo", status: "active", progress: 71, rating: 4.4, verified: true, tags: ["Photography", "Videography"], teamMembers: [{ name: "Nina Smith", role: "Photographer" }, { name: "Owen Johnson", role: "Videographer" }] },77 { id: 9, name: "Sage Thompson", website: "sagethompson.ux", status: "active", progress: 89, rating: 4.7, verified: true, tags: ["UX Design", "UI Design"], teamMembers: [{ name: "Pete White", role: "UX Lead" }, { name: "Quinn Brown", role: "UI Designer" }] },78 { id: 10, name: "River Davis", website: "riverdavis.content", status: "pending", progress: 45, rating: 4.1, verified: false, tags: ["Content Strategy", "Copywriting"], teamMembers: [{ name: "Riley Adams", role: "Content Writer" }] },79 { id: 11, name: "Phoenix Williams", website: "phoenixwilliams.digital", status: "active", progress: 93, rating: 4.8, verified: true, tags: ["Digital Consulting", "Strategy"], teamMembers: [{ name: "Sofia Lee", role: "Consultant" }, { name: "Tucker Brown", role: "Digital Strategist" }] },80 { id: 12, name: "Atlas Johnson", website: "atlasjohnson.brand", status: "inactive", progress: 28, rating: 3.6, verified: false, tags: ["Brand Design", "Graphic Design"], teamMembers: [{ name: "Uma Patel", role: "Graphic Designer" }] },81];8283export const cellRendererHeaders: AngularColumnDef[] = [84 { accessor: "id", label: "ID", width: 60, type: "number" },85 { accessor: "name", label: "Name", width: 180, type: "string" },86 { accessor: "teamMembers", label: "Team", width: 280, type: "string" },87 { accessor: "website", label: "Website", width: 180, type: "string" },88 { accessor: "status", label: "Status", width: 120, type: "string" },89 { accessor: "progress", label: "Progress", width: 150, type: "number" },90 { accessor: "rating", label: "Rating", width: 150, type: "number" },91 { accessor: "verified", label: "Verified", width: 100, type: "boolean" },92 { accessor: "tags", label: "Tags", width: 250, type: "string" },93];9495export const cellRendererConfig = {96 headers: cellRendererHeaders,97 rows: cellRendererData,98 tableProps: {99 selectableCells: true,100 customTheme: { rowHeight: 48 },101 },102} as const;103104105// cr-progress-cell.component.ts106import { Component, Input } from "@angular/core";107import type { CellValue } from "@simple-table/angular";108109@Component({110 standalone: true,111 selector: "demo-cr-progress",112 template: `113 <div>114 <div style="font-size:12px;margin-bottom:2px;">{{ pct }}%</div>115 <div style="height:10px;background:#E5E7EB;border-radius:5px;overflow:hidden;">116 <div [style.width.%]="pct" [style.background]="barColor" style="height:100%;border-radius:5px;transition:width 0.3s;"></div>117 </div>118 </div>119 `,120})121export class CrProgressCellComponent {122 @Input() value!: CellValue;123124 get pct(): number {125 return Number(this.value) || 0;126 }127128 get barColor(): string {129 const n = this.pct;130 if (n < 30) return "#EF4444";131 if (n < 70) return "#F59E0B";132 return "#10B981";133 }134}135136137// cr-rating-cell.component.ts138import { Component, Input } from "@angular/core";139import type { CellValue } from "@simple-table/angular";140141@Component({142 standalone: true,143 selector: "demo-cr-rating",144 template: `145 <span style="display:flex;align-items:center;gap:4px;">146 <span style="color:#F59E0B;letter-spacing:1px;">147 @for (_ of repeat(fullCount); track $index) {148 ★149 }150 @if (hasHalf) {151 <span style="opacity:0.5;">★</span>152 }153 @for (_ of repeat(emptyCount); track $index) {154 ☆155 }156 </span>157 <span style="font-size:12px;color:#6b7280;">{{ rating }}</span>158 </span>159 `,160})161export class CrRatingCellComponent {162 @Input() value!: CellValue;163164 get rating(): number {165 return Number(this.value) || 0;166 }167168 get fullCount(): number {169 return Math.floor(this.rating);170 }171172 get hasHalf(): boolean {173 return this.rating % 1 >= 0.25;174 }175176 get emptyCount(): number {177 return Math.max(0, 5 - this.fullCount - (this.hasHalf ? 1 : 0));178 }179180 repeat(n: number): number[] {181 return Array.from({ length: n }, (_, i) => i);182 }183}184185186// cr-status-cell.component.ts187import { Component, Input } from "@angular/core";188import type { CellValue } from "@simple-table/angular";189190@Component({191 standalone: true,192 selector: "demo-cr-status",193 template: `194 <span [style.color]="meta.color" style="font-weight:600;text-transform:capitalize;">{{ meta.icon }} {{ status }}</span>195 `,196})197export class CrStatusCellComponent {198 @Input() value!: CellValue;199200 get status(): string {201 return String(this.value);202 }203204 get meta(): { icon: string; color: string } {205 const map: Record<string, { icon: string; color: string }> = {206 active: { icon: "✓", color: "#10B981" },207 inactive: { icon: "✕", color: "#EF4444" },208 pending: { icon: "!", color: "#F59E0B" },209 };210 return map[this.status] ?? { icon: "?", color: "#6b7280" };211 }212}213214215// cr-tags-cell.component.ts216import { Component, Input } from "@angular/core";217import type { CellValue } from "@simple-table/angular";218219@Component({220 standalone: true,221 selector: "demo-cr-tags",222 template: `223 <div style="display:flex;gap:4px;flex-wrap:nowrap;overflow:hidden;">224 @for (tag of tags; track tag) {225 <span226 style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500;background:#DBEAFE;color:#1E40AF;white-space:nowrap;"227 >{{ tag }}</span>228 }229 </div>230 `,231})232export class CrTagsCellComponent {233 @Input() value!: CellValue;234235 get tags(): string[] {236 return Array.isArray(this.value) ? (this.value as string[]) : [];237 }238}239240241// cr-team-members-cell.component.ts242import { Component, Input } from "@angular/core";243import type { Row } from "@simple-table/angular";244import type { CellRendererEmployee } from "./cell-renderer.demo-data";245246@Component({247 standalone: true,248 selector: "demo-cr-team-members",249 template: `250 <div style="display:flex;align-items:center;gap:6px;">251 @for (m of members; track m.name) {252 <div style="display:flex;align-items:center;gap:4px;">253 <div254 style="width:24px;height:24px;border-radius:50%;background:#DBEAFE;color:#1E40AF;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0;"255 >256 {{ initials(m.name) }}257 </div>258 <span style="font-size:13px;white-space:nowrap;">{{ m.name }}</span>259 </div>260 }261 </div>262 `,263})264export class CrTeamMembersCellComponent {265 @Input({ required: true }) row!: Row;266267 get members(): { name: string; role: string }[] {268 return (this.row as unknown as CellRendererEmployee).teamMembers;269 }270271 initials(name: string): string {272 return name.split(" ").map((n) => n[0]).join("").toUpperCase();273 }274}275276277// cr-verified-cell.component.ts278import { Component, Input } from "@angular/core";279import type { CellValue } from "@simple-table/angular";280281@Component({282 standalone: true,283 selector: "demo-cr-verified",284 template: `285 <span [style.color]="yes ? '#10B981' : '#EF4444'" style="font-weight:600;">{{ yes ? "✓ Yes" : "✕ No" }}</span>286 `,287})288export class CrVerifiedCellComponent {289 @Input() value!: CellValue;290291 get yes(): boolean {292 return Boolean(this.value);293 }294}295296297// cr-website-cell.component.ts298import { Component, Input } from "@angular/core";299import type { CellValue } from "@simple-table/angular";300301@Component({302 standalone: true,303 selector: "demo-cr-website",304 template: `305 <span>🌐 <a class="crw" [href]="'https://' + url" target="_blank" rel="noopener noreferrer">{{ url }}</a></span>306 `,307 styles: [308 `309 .crw {310 color: #2563eb;311 text-decoration: none;312 }313 .crw:hover {314 text-decoration: underline;315 }316 `,317 ],318})319export class CrWebsiteCellComponent {320 @Input() value!: CellValue;321322 get url(): string {323 return String(this.value);324 }325}326
SvelteCellRendererDemo.svelte
Copy
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, SvelteColumnDef } from "@simple-table/svelte";4 import { cellRendererConfig } from "./cell-renderer.demo-data";5 import CrTeamMembersCell from "./CrTeamMembersCell.svelte";6 import CrWebsiteCell from "./CrWebsiteCell.svelte";7 import CrStatusCell from "./CrStatusCell.svelte";8 import CrProgressCell from "./CrProgressCell.svelte";9 import CrRatingCell from "./CrRatingCell.svelte";10 import CrVerifiedCell from "./CrVerifiedCell.svelte";11 import CrTagsCell from "./CrTagsCell.svelte";12 import "@simple-table/svelte/styles.css";1314 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();1516 const RENDERERS: Record<string, unknown> = {17 teamMembers: CrTeamMembersCell,18 website: CrWebsiteCell,19 status: CrStatusCell,20 progress: CrProgressCell,21 rating: CrRatingCell,22 verified: CrVerifiedCell,23 tags: CrTagsCell,24 };2526 const headers: SvelteColumnDef[] = cellRendererConfig.headers.map((h) => {27 const cellRenderer = RENDERERS[h.accessor as string];28 return cellRenderer ? { ...h, cellRenderer } : { ...h };29 });30</script>3132<SimpleTable33 columns={headers}34 rows={cellRendererConfig.rows}35 {height}36 {theme}37 selectableCells={cellRendererConfig.tableProps.selectableCells}38 customTheme={cellRendererConfig.tableProps.customTheme}39/>404142// CrProgressCell.svelte43<script lang="ts">44 import type { CellRendererProps } from "@simple-table/svelte";4546 let { value }: CellRendererProps = $props();47 const pct = $derived(Number(value) || 0);48 const color = $derived(pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981");49</script>5051<div>52 <div style="font-size:12px;margin-bottom:2px;">{pct}%</div>53 <div style="height:10px;background:#E5E7EB;border-radius:5px;overflow:hidden;">54 <div style="width:{pct}%;height:100%;background:{color};border-radius:5px;transition:width 0.3s;"></div>55 </div>56</div>575859// CrRatingCell.svelte60<script lang="ts">61 import type { CellRendererProps } from "@simple-table/svelte";6263 let { value }: CellRendererProps = $props();64 const rating = $derived(Number(value) || 0);65 const full = $derived(Math.floor(rating));66 const hasHalf = $derived(rating % 1 >= 0.25);67 const empty = $derived(5 - full - (hasHalf ? 1 : 0));68</script>6970<span style="display:flex;align-items:center;gap:4px;">71 <span style="color:#F59E0B;letter-spacing:1px;">72 {"★".repeat(full)}{#if hasHalf}<span style="opacity:0.5;">★</span>{/if}{"☆".repeat(Math.max(0, empty))}73 </span>74 <span style="font-size:12px;color:#6b7280;">{rating}</span>75</span>767778// CrStatusCell.svelte79<script lang="ts">80 import type { CellRendererProps } from "@simple-table/svelte";8182 let { value }: CellRendererProps = $props();83 const status = $derived(String(value));84 const meta = $derived.by(() => {85 const map: Record<string, { icon: string; color: string }> = {86 active: { icon: "✓", color: "#10B981" },87 inactive: { icon: "✕", color: "#EF4444" },88 pending: { icon: "!", color: "#F59E0B" },89 };90 return map[status] ?? { icon: "?", color: "#6b7280" };91 });92</script>9394<span style="color:{meta.color};font-weight:600;text-transform:capitalize;">{meta.icon} {status}</span>959697// CrTagsCell.svelte98<script lang="ts">99 import type { CellRendererProps } from "@simple-table/svelte";100101 let { value }: CellRendererProps = $props();102 const tags = $derived(Array.isArray(value) ? (value as string[]) : []);103</script>104105<div style="display:flex;gap:4px;flex-wrap:nowrap;overflow:hidden;">106 {#each tags as tag (tag)}107 <span108 style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500;background:#DBEAFE;color:#1E40AF;white-space:nowrap;"109 >{tag}</span>110 {/each}111</div>112113114// CrTeamMembersCell.svelte115<script lang="ts">116 import type { CellRendererProps } from "@simple-table/svelte";117 import type { CellRendererEmployee } from "./cell-renderer.demo-data";118119 let { row }: CellRendererProps = $props();120 const members = $derived((row as unknown as CellRendererEmployee).teamMembers);121122 function initials(name: string): string {123 return name.split(" ").map((n) => n[0]).join("").toUpperCase();124 }125</script>126127<div style="display:flex;align-items:center;gap:6px;">128 {#each members as m (m.name)}129 <div style="display:flex;align-items:center;gap:4px;">130 <div131 style="width:24px;height:24px;border-radius:50%;background:#DBEAFE;color:#1E40AF;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0;"132 >133 {initials(m.name)}134 </div>135 <span style="font-size:13px;white-space:nowrap;">{m.name}</span>136 </div>137 {/each}138</div>139140141// CrVerifiedCell.svelte142<script lang="ts">143 import type { CellRendererProps } from "@simple-table/svelte";144145 let { value }: CellRendererProps = $props();146 const yes = $derived(Boolean(value));147</script>148149<span style="color:{yes ? '#10B981' : '#EF4444'};font-weight:600;">{yes ? "✓ Yes" : "✕ No"}</span>150151152// CrWebsiteCell.svelte153<script lang="ts">154 import type { CellRendererProps } from "@simple-table/svelte";155156 let { value }: CellRendererProps = $props();157 const url = $derived(String(value));158</script>159160<span>🌐 <a class="crw" href="https://{url}" target="_blank" rel="noopener noreferrer">{url}</a></span>161162<style>163 .crw {164 color: #2563eb;165 text-decoration: none;166 }167 .crw:hover {168 text-decoration: underline;169 }170</style>171
Solid TSX
Copy
1import {SimpleTable} from "@simple-table/solid";import type { Theme, SolidColumnDef, CellRendererProps } from "@simple-table/solid";2import { cellRendererConfig } from "./cell-renderer.demo-data";3import type { CellRendererEmployee } from "./cell-renderer.demo-data";4import "@simple-table/solid/styles.css";56const getInitials = (name: string) =>7 name.split(" ").map((n) => n[0]).join("").toUpperCase();89const TeamCell = (props: CellRendererProps) => {10 const members = (props.row as CellRendererEmployee).teamMembers;11 return (12 <div style={{ display: "flex", "align-items": "center", gap: "6px" }}>13 {members.map((m) => (14 <div style={{ display: "flex", "align-items": "center", gap: "4px" }}>15 <div16 style={{17 width: "24px",18 height: "24px",19 "border-radius": "50%",20 background: "#DBEAFE",21 color: "#1E40AF",22 display: "flex",23 "align-items": "center",24 "justify-content": "center",25 "font-size": "10px",26 "font-weight": "600",27 "flex-shrink": "0",28 }}29 >30 {getInitials(m.name)}31 </div>32 <span style={{ "font-size": "13px", "white-space": "nowrap" }}>{m.name}</span>33 </div>34 ))}35 </div>36 );37};3839const WebsiteCell = (props: CellRendererProps) => {40 const url = String(props.value);41 return (42 <span>43 🌐{" "}44 <a45 href={`https://${url}`}46 target="_blank"47 rel="noopener noreferrer"48 style={{ color: "#2563EB", "text-decoration": "none" }}49 onMouseEnter={(e) => (e.currentTarget.style.textDecoration = "underline")}50 onMouseLeave={(e) => (e.currentTarget.style.textDecoration = "none")}51 >52 {url}53 </a>54 </span>55 );56};5758const StatusCell = (props: CellRendererProps) => {59 const status = String(props.value);60 const map: Record<string, { icon: string; color: string }> = {61 active: { icon: "✓", color: "#10B981" },62 inactive: { icon: "✕", color: "#EF4444" },63 pending: { icon: "!", color: "#F59E0B" },64 };65 const { icon, color } = map[status] ?? { icon: "?", color: "#6b7280" };66 return (67 <span style={{ color, "font-weight": "600", "text-transform": "capitalize" }}>68 {icon} {status}69 </span>70 );71};7273const ProgressCell = (props: CellRendererProps) => {74 const pct = Number(props.value) || 0;75 const color = pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981";76 return (77 <div>78 <div style={{ "font-size": "12px", "margin-bottom": "2px" }}>{pct}%</div>79 <div style={{ height: "10px", background: "#E5E7EB", "border-radius": "5px", overflow: "hidden" }}>80 <div81 style={{82 width: `${pct}%`,83 height: "100%",84 background: color,85 "border-radius": "5px",86 transition: "width 0.3s",87 }}88 />89 </div>90 </div>91 );92};9394const RatingCell = (props: CellRendererProps) => {95 const rating = Number(props.value) || 0;96 const full = Math.floor(rating);97 const hasHalf = rating % 1 >= 0.25;98 const empty = 5 - full - (hasHalf ? 1 : 0);99 return (100 <span style={{ display: "flex", "align-items": "center", gap: "4px" }}>101 <span style={{ color: "#F59E0B", "letter-spacing": "1px" }}>102 {"★".repeat(full)}103 {hasHalf && <span style={{ opacity: "0.5" }}>★</span>}104 {"☆".repeat(Math.max(0, empty))}105 </span>106 <span style={{ "font-size": "12px", color: "#6b7280" }}>{rating}</span>107 </span>108 );109};110111const VerifiedCell = (props: CellRendererProps) => {112 const yes = Boolean(props.value);113 return (114 <span style={{ color: yes ? "#10B981" : "#EF4444", "font-weight": "600" }}>115 {yes ? "✓ Yes" : "✕ No"}116 </span>117 );118};119120const TagsCell = (props: CellRendererProps) => {121 const tags = Array.isArray(props.value) ? (props.value as string[]) : [];122 return (123 <div style={{ display: "flex", gap: "4px", "flex-wrap": "nowrap", overflow: "hidden" }}>124 {tags.map((tag) => (125 <span126 style={{127 display: "inline-block",128 padding: "2px 8px",129 "border-radius": "4px",130 "font-size": "12px",131 "font-weight": "500",132 background: "#DBEAFE",133 color: "#1E40AF",134 "white-space": "nowrap",135 }}136 >137 {tag}138 </span>139 ))}140 </div>141 );142};143144const RENDERER_MAP: Record<string, (props: CellRendererProps) => unknown> = {145 teamMembers: TeamCell,146 website: WebsiteCell,147 status: StatusCell,148 progress: ProgressCell,149 rating: RatingCell,150 verified: VerifiedCell,151 tags: TagsCell,152};153154const HEADERS: SolidColumnDef[] = cellRendererConfig.headers.map((h) => {155 const fn = RENDERER_MAP[String(h.accessor)];156 return fn !== undefined ? { ...h, cellRenderer: fn } : h;157});158159export default function CellRendererDemo(props: { height?: string | number; theme?: Theme }) {160 return (161 <SimpleTable162 columns={HEADERS}163 rows={cellRendererConfig.rows}164 height={props.height ?? "400px"}165 theme={props.theme}166 selectableCells={cellRendererConfig.tableProps.selectableCells}167 customTheme={cellRendererConfig.tableProps.customTheme}168 />169 );170}
TypeScriptCellRendererDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme, ColumnDef, CellRenderer } from "simple-table-core";3import { cellRendererConfig } from "./cell-renderer.demo-data";4import type { CellRendererEmployee } from "./cell-renderer.demo-data";5import "simple-table-core/styles.css";67const html = (str: string): Node => {8 const t = document.createElement("template");9 t.innerHTML = str.trim();10 return t.content;11};1213const getInitials = (name: string) =>14 name.split(" ").map((n) => n[0]).join("").toUpperCase();1516const RENDERERS: Record<string, CellRenderer> = {17 teamMembers: ({ row }) => {18 const members = (row as CellRendererEmployee).teamMembers;19 return html(20 `<div style="display:flex;align-items:center;gap:6px">${members21 .map(22 (m) =>23 `<div style="display:flex;align-items:center;gap:4px"><div style="width:24px;height:24px;border-radius:50%;background:#DBEAFE;color:#1E40AF;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0">${getInitials(m.name)}</div><span style="font-size:13px;white-space:nowrap">${m.name}</span></div>`,24 )25 .join("")}</div>`,26 );27 },2829 website: ({ value }) => {30 const url = String(value);31 return html(32 `<span>🌐 <a href="https://${url}" target="_blank" rel="noopener noreferrer" style="color:#2563EB;text-decoration:none" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">${url}</a></span>`,33 );34 },3536 status: ({ value }) => {37 const status = String(value);38 const map: Record<string, { icon: string; color: string }> = {39 active: { icon: "✓", color: "#10B981" },40 inactive: { icon: "✕", color: "#EF4444" },41 pending: { icon: "!", color: "#F59E0B" },42 };43 const { icon, color } = map[status] ?? { icon: "?", color: "#6b7280" };44 return html(45 `<span style="color:${color};font-weight:600;text-transform:capitalize">${icon} ${status}</span>`,46 );47 },4849 progress: ({ value }) => {50 const pct = Number(value) || 0;51 const color = pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981";52 return html(53 `<div><div style="font-size:12px;margin-bottom:2px">${pct}%</div><div style="height:10px;background:#E5E7EB;border-radius:5px;overflow:hidden"><div style="width:${pct}%;height:100%;background:${color};border-radius:5px;transition:width 0.3s"></div></div></div>`,54 );55 },5657 rating: ({ value }) => {58 const rating = Number(value) || 0;59 const full = Math.floor(rating);60 const hasHalf = rating % 1 >= 0.25;61 const empty = 5 - full - (hasHalf ? 1 : 0);62 const halfStar = hasHalf ? '<span style="opacity:0.5">★</span>' : "";63 return html(64 `<span style="display:flex;align-items:center;gap:4px"><span style="color:#F59E0B;letter-spacing:1px">${"★".repeat(full)}${halfStar}${"☆".repeat(Math.max(0, empty))}</span><span style="font-size:12px;color:#6b7280">${rating}</span></span>`,65 );66 },6768 verified: ({ value }) => {69 const yes = Boolean(value);70 return html(71 `<span style="color:${yes ? "#10B981" : "#EF4444"};font-weight:600">${yes ? "✓ Yes" : "✕ No"}</span>`,72 );73 },7475 tags: ({ value }) => {76 const tags = Array.isArray(value) ? (value as string[]) : [];77 return html(78 `<div style="display:flex;gap:4px;flex-wrap:nowrap;overflow:hidden">${tags79 .map(80 (tag) =>81 `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500;background:#DBEAFE;color:#1E40AF;white-space:nowrap">${tag}</span>`,82 )83 .join("")}</div>`,84 );85 },86};8788export function renderCellRendererDemo(89 container: HTMLElement,90 options?: { height?: string | number; theme?: Theme },91): SimpleTableVanilla {92 const headers: ColumnDef[] = cellRendererConfig.headers.map((h) => {93 const renderer = RENDERERS[h.accessor as string];94 return renderer ? { ...h, cellRenderer: renderer } : { ...h };95 });9697 const table = new SimpleTableVanilla(container, {98 columns: headers,99 rows: cellRendererConfig.rows,100 height: options?.height ?? "400px",101 theme: options?.theme,102 selectableCells: cellRendererConfig.tableProps.selectableCells,103 customTheme: cellRendererConfig.tableProps.customTheme,104 });105 return table;106}107108109// cell-renderer.demo-data.ts110// Self-contained demo table setup for this example.111import type { ColumnDef } from "simple-table-core";112113114export type CellRendererEmployee = {115 id: number;116 name: string;117 website: string;118 status: string;119 progress: number;120 rating: number;121 verified: boolean;122 tags: string[];123 teamMembers: { name: string; role: string }[];124};125126export const cellRendererData: CellRendererEmployee[] = [127 { id: 1, name: "Isabella Romano", website: "isabellaromano.design", status: "active", progress: 92, rating: 4.9, verified: true, tags: ["UI/UX", "Design", "Frontend"], teamMembers: [{ name: "Alice Smith", role: "Designer" }, { name: "Bob Johnson", role: "Developer" }] },128 { id: 2, name: "Ethan McKenzie", website: "ethanmckenzie.dev", status: "active", progress: 87, rating: 4.7, verified: true, tags: ["Web Development", "Backend", "API"], teamMembers: [{ name: "Charlie Brown", role: "Backend Developer" }, { name: "Diana Prince", role: "Frontend Developer" }] },129 { id: 3, name: "Zoe Patterson", website: "zoepatterson.com", status: "pending", progress: 34, rating: 4.2, verified: false, tags: ["Branding", "Marketing"], teamMembers: [{ name: "Eve Adams", role: "Marketing Manager" }] },130 { id: 4, name: "Felix Chang", website: "felixchang.mobile", status: "active", progress: 95, rating: 4.8, verified: true, tags: ["Mobile App", "UX/UI"], teamMembers: [{ name: "Grace Lee", role: "UX Designer" }, { name: "Hank Johnson", role: "Mobile Developer" }] },131 { id: 5, name: "Aria Gonzalez", website: "ariagonzalez.writer", status: "active", progress: 78, rating: 4.6, verified: true, tags: ["Content Writing", "Copywriting"], teamMembers: [{ name: "Ivy White", role: "Content Strategist" }] },132 { id: 6, name: "Jasper Flynn", website: "jasperflynn.tech", status: "inactive", progress: 12, rating: 3.8, verified: false, tags: ["Consulting", "Tech Strategy"], teamMembers: [{ name: "Kate Brown", role: "Consultant" }] },133 { id: 7, name: "Nova Sterling", website: "novasterling.marketing", status: "active", progress: 83, rating: 4.5, verified: true, tags: ["Digital Marketing", "SEO"], teamMembers: [{ name: "Leo Wilson", role: "SEO Specialist" }, { name: "Mia Davis", role: "Marketing Analyst" }] },134 { id: 8, name: "Cruz Martinez", website: "cruzmartinez.photo", status: "active", progress: 71, rating: 4.4, verified: true, tags: ["Photography", "Videography"], teamMembers: [{ name: "Nina Smith", role: "Photographer" }, { name: "Owen Johnson", role: "Videographer" }] },135 { id: 9, name: "Sage Thompson", website: "sagethompson.ux", status: "active", progress: 89, rating: 4.7, verified: true, tags: ["UX Design", "UI Design"], teamMembers: [{ name: "Pete White", role: "UX Lead" }, { name: "Quinn Brown", role: "UI Designer" }] },136 { id: 10, name: "River Davis", website: "riverdavis.content", status: "pending", progress: 45, rating: 4.1, verified: false, tags: ["Content Strategy", "Copywriting"], teamMembers: [{ name: "Riley Adams", role: "Content Writer" }] },137 { id: 11, name: "Phoenix Williams", website: "phoenixwilliams.digital", status: "active", progress: 93, rating: 4.8, verified: true, tags: ["Digital Consulting", "Strategy"], teamMembers: [{ name: "Sofia Lee", role: "Consultant" }, { name: "Tucker Brown", role: "Digital Strategist" }] },138 { id: 12, name: "Atlas Johnson", website: "atlasjohnson.brand", status: "inactive", progress: 28, rating: 3.6, verified: false, tags: ["Brand Design", "Graphic Design"], teamMembers: [{ name: "Uma Patel", role: "Graphic Designer" }] },139];140141export const cellRendererHeaders: ColumnDef[] = [142 { accessor: "id", label: "ID", width: 60, type: "number" },143 { accessor: "name", label: "Name", width: 180, type: "string" },144 { accessor: "teamMembers", label: "Team", width: 280, type: "string" },145 { accessor: "website", label: "Website", width: 180, type: "string" },146 { accessor: "status", label: "Status", width: 120, type: "string" },147 { accessor: "progress", label: "Progress", width: 150, type: "number" },148 { accessor: "rating", label: "Rating", width: 150, type: "number" },149 { accessor: "verified", label: "Verified", width: 100, type: "boolean" },150 { accessor: "tags", label: "Tags", width: 250, type: "string" },151];152153export const cellRendererConfig = {154 headers: cellRendererHeaders,155 rows: cellRendererData,156 tableProps: {157 selectableCells: true,158 customTheme: { rowHeight: 48 },159 },160} as const;161
Props
Cell Renderer Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
ColumnDef.cellRenderer | Optional | Custom cell content. Framework adapters accept components or render functions; vanilla returns string/number/null or a DOM Node. |
Renderer arguments
CellRendererProps
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
accessor | Required | The column accessor/key for the cell being rendered | |
colIndexnumber | Required | The column index (0-based) | |
row | Required | The complete row object containing all data for this row | |
rowIndexnumber | Required | The row index (0-based) | |
rowPath(string | number)[] | Optional | Array path through the nested data structure to reach this row. Each element is either a number (array index) or string (property name). Useful for accessing nested/hierarchical data. | |
theme | Required | Current theme of the table | |
value | Required | The raw cell value | |
formattedValuestring | number | string[] | number[] | boolean | null | undefined | Optional | The formatted cell value (output from valueFormatter if defined). Use this for display purposes when you need both raw and formatted values. |