SimpleTable renders the HTML table structure — <table>, <thead>, <tbody>, <tfoot> — with Frontile's styling, for manual composition when you want full control over layout instead of automatic rendering from data.
Key Features:
For automatic rendering with sticky elements and data management, use Table instead.
import { SimpleTable } from 'frontile';
SimpleTable uses block form composition where you manually define the table structure:
| ID | Name | Role | |
|---|---|---|---|
| 1 | John Doe | john@example.com | admin |
| 2 | Jane Smith | jane@example.com | user |
import Component from '@glimmer/component';
import { SimpleTable } from 'frontile';
export default class DemoComponent extends Component {
items = [
{ id: '1', name: 'John Doe', email: 'john@example.com', role: 'admin' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'user' }
];
<template>
<div class='demo-stack demo-stack--wide'>
<SimpleTable as |t|>
<t.Header>
<t.Column>ID</t.Column>
<t.Column>Name</t.Column>
<t.Column>Email</t.Column>
<t.Column>Role</t.Column>
</t.Header>
<t.Body>
{{#each this.items as |item|}}
<t.Row>
<t.Cell>{{item.id}}</t.Cell>
<t.Cell>{{item.name}}</t.Cell>
<t.Cell>{{item.email}}</t.Cell>
<t.Cell>{{item.role}}</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
</template>
}
SimpleTable yields Header, Body, Footer, Column, Row, and Cell components. Compose
those pieces in native table order; unlike Table, SimpleTable does not create columns or rows
from data for you.
Create complex header layouts with custom content:
| 👤 User Info | 📧 Contact | 🔄 Status | ⚡ Actions |
|---|---|---|---|
|
John Doe
ID: 1
|
john@example.com |
Active
|
|
|
Jane Smith
ID: 2
|
jane@example.com |
Inactive
|
|
|
Bob Wilson
ID: 3
|
bob@example.com |
Pending
|
import Component from '@glimmer/component';
import { SimpleTable } from 'frontile';
import { Button, Chip } from 'frontile';
export default class DemoComponent extends Component {
users = [
{
id: '1',
name: 'John Doe',
email: 'john@example.com',
status: 'Active',
chipColor: 'success'
},
{
id: '2',
name: 'Jane Smith',
email: 'jane@example.com',
status: 'Inactive',
chipColor: 'neutral'
},
{
id: '3',
name: 'Bob Wilson',
email: 'bob@example.com',
status: 'Pending',
chipColor: 'warning'
}
];
<template>
<div class='demo-stack demo-stack--wide'>
<SimpleTable as |t|>
<t.Header>
<t.Column>
<span class='flex items-center gap-2'>
👤 User Info
</span>
</t.Column>
<t.Column>
<span class='flex items-center gap-2'>
📧 Contact
</span>
</t.Column>
<t.Column>
<span class='flex items-center gap-2'>
🔄 Status
</span>
</t.Column>
<t.Column>
<span class='flex items-center gap-2'>
âš¡ Actions
</span>
</t.Column>
</t.Header>
<t.Body>
{{#each this.users as |user|}}
<t.Row>
<t.Cell>
<div>
<div class='font-medium'>{{user.name}}</div>
<div class='text-sm text-neutral-soft'>ID: {{user.id}}</div>
</div>
</t.Cell>
<t.Cell>{{user.email}}</t.Cell>
<t.Cell>
<Chip @color={{user.chipColor}} @size='sm'>
{{user.status}}
</Chip>
</t.Cell>
<t.Cell>
<Button @variant='plain' @size='sm'>
Edit
</Button>
</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
</template>
}
SimpleTable excels at complex cell layouts and interactive content:
| Product | Price | Stock | Actions |
|---|---|---|---|
|
Wireless Headphones
ID:
1
|
$99.99 |
15 units
In Stock
|
|
|
Smart Watch
ID:
2
|
$299.99 |
3 units
Low Stock
|
|
|
Bluetooth Speaker
ID:
3
|
$59.99 |
0 units
Out of Stock
|
|
import Component from '@glimmer/component';
import { SimpleTable } from 'frontile';
import { Button, Chip } from 'frontile';
export default class DemoComponent extends Component {
products = [
{
id: '1',
name: 'Wireless Headphones',
price: 99.99,
stock: 15,
stockStatus: 'In Stock',
stockChipColor: 'success'
},
{
id: '2',
name: 'Smart Watch',
price: 299.99,
stock: 3,
stockStatus: 'Low Stock',
stockChipColor: 'warning'
},
{
id: '3',
name: 'Bluetooth Speaker',
price: 59.99,
stock: 0,
stockStatus: 'Out of Stock',
stockChipColor: 'danger'
}
];
<template>
<div class='demo-stack demo-stack--wide'>
<SimpleTable as |t|>
<t.Header>
<t.Column>Product</t.Column>
<t.Column>Price</t.Column>
<t.Column>Stock</t.Column>
<t.Column>Actions</t.Column>
</t.Header>
<t.Body>
{{#each this.products as |product|}}
<t.Row>
<t.Cell>
<div>
<div class='font-medium'>{{product.name}}</div>
<div class='text-sm text-neutral-soft'>ID:
{{product.id}}</div>
</div>
</t.Cell>
<t.Cell>
<span class='font-medium'>${{product.price}}</span>
</t.Cell>
<t.Cell>
<div class='flex flex-col gap-1'>
<div class='font-medium'>{{product.stock}} units</div>
<Chip @color={{product.stockChipColor}} @size='sm'>
{{product.stockStatus}}
</Chip>
</div>
</t.Cell>
<t.Cell>
<div class='flex gap-2'>
<Button @color='primary' @size='sm'>
Edit
</Button>
<Button @color='danger' @size='sm'>
Delete
</Button>
</div>
</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
</template>
}
Control table spacing with size variants:
| Name | Role |
|---|---|
| John Doe | Developer |
| Jane Smith | Designer |
| Name | Role |
|---|---|
| John Doe | Developer |
| Jane Smith | Designer |
| Name | Role |
|---|---|
| John Doe | Developer |
| Jane Smith | Designer |
import Component from '@glimmer/component';
import { SimpleTable } from 'frontile';
export default class DemoComponent extends Component {
data = [
{ name: 'John Doe', role: 'Developer' },
{ name: 'Jane Smith', role: 'Designer' }
];
<template>
<div class='demo-stack demo-stack--wide'>
<div class='space-y-6'>
<div>
<h4 class='font-medium mb-2'>Small (sm) - Compact spacing</h4>
<SimpleTable @size='sm' as |t|>
<t.Header>
<t.Column>Name</t.Column>
<t.Column>Role</t.Column>
</t.Header>
<t.Body>
{{#each this.data as |item|}}
<t.Row>
<t.Cell>{{item.name}}</t.Cell>
<t.Cell>{{item.role}}</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
<div>
<h4 class='font-medium mb-2'>Medium (md) - Default spacing</h4>
<SimpleTable @size='md' as |t|>
<t.Header>
<t.Column>Name</t.Column>
<t.Column>Role</t.Column>
</t.Header>
<t.Body>
{{#each this.data as |item|}}
<t.Row>
<t.Cell>{{item.name}}</t.Cell>
<t.Cell>{{item.role}}</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
<div>
<h4 class='font-medium mb-2'>Large (lg) - Spacious layout</h4>
<SimpleTable @size='lg' as |t|>
<t.Header>
<t.Column>Name</t.Column>
<t.Column>Role</t.Column>
</t.Header>
<t.Body>
{{#each this.data as |item|}}
<t.Row>
<t.Cell>{{item.name}}</t.Cell>
<t.Cell>{{item.role}}</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
</div>
</div>
</template>
}
Control column sizing behavior:
| ID | Name | Department | |
|---|---|---|---|
| 1 | John Doe | john.doe.longname@example-company.com | Engineering |
| 2 | Jane | jane@ex.co | Design |
| ID | Name | Department | |
|---|---|---|---|
| 1 | John Doe | john.doe.longname@example-company.com | Engineering |
| 2 | Jane | jane@ex.co | Design |
import Component from '@glimmer/component';
import { SimpleTable } from 'frontile';
export default class DemoComponent extends Component {
data = [
{
id: '1',
name: 'John Doe',
email: 'john.doe.longname@example-company.com',
department: 'Engineering'
},
{
id: '2',
name: 'Jane',
email: 'jane@ex.co',
department: 'Design'
}
];
<template>
<div class='demo-stack demo-stack--wide'>
<div class='space-y-6'>
<div>
<h4 class='font-medium mb-2'>Auto Layout (default) - Content-based
sizing</h4>
<SimpleTable @layout='auto' as |t|>
<t.Header>
<t.Column>ID</t.Column>
<t.Column>Name</t.Column>
<t.Column>Email</t.Column>
<t.Column>Department</t.Column>
</t.Header>
<t.Body>
{{#each this.data as |item|}}
<t.Row>
<t.Cell>{{item.id}}</t.Cell>
<t.Cell>{{item.name}}</t.Cell>
<t.Cell>{{item.email}}</t.Cell>
<t.Cell>{{item.department}}</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
<div>
<h4 class='font-medium mb-2'>Fixed Layout - Equal column widths</h4>
<SimpleTable @layout='fixed' as |t|>
<t.Header>
<t.Column>ID</t.Column>
<t.Column>Name</t.Column>
<t.Column>Email</t.Column>
<t.Column>Department</t.Column>
</t.Header>
<t.Body>
{{#each this.data as |item|}}
<t.Row>
<t.Cell>{{item.id}}</t.Cell>
<t.Cell>{{item.name}}</t.Cell>
<t.Cell>{{item.email}}</t.Cell>
<t.Cell>{{item.department}}</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
</div>
</div>
</template>
}
Enable alternating row colors for better readability:
| Name | Status | |
|---|---|---|
| John Doe | john@example.com |
Active
|
| Jane Smith | jane@example.com |
Active
|
| Bob Johnson | bob@example.com |
Inactive
|
| Alice Brown | alice@example.com |
Active
|
import Component from '@glimmer/component';
import { SimpleTable } from 'frontile';
import { Chip } from 'frontile';
export default class DemoComponent extends Component {
users = [
{
name: 'John Doe',
email: 'john@example.com',
status: 'Active',
statusColor: 'success'
},
{
name: 'Jane Smith',
email: 'jane@example.com',
status: 'Active',
statusColor: 'success'
},
{
name: 'Bob Johnson',
email: 'bob@example.com',
status: 'Inactive',
statusColor: 'neutral'
},
{
name: 'Alice Brown',
email: 'alice@example.com',
status: 'Active',
statusColor: 'success'
}
];
<template>
<div class='demo-stack demo-stack--wide'>
<SimpleTable @isStriped={{true}} as |t|>
<t.Header>
<t.Column>Name</t.Column>
<t.Column>Email</t.Column>
<t.Column>Status</t.Column>
</t.Header>
<t.Body>
{{#each this.users as |user|}}
<t.Row>
<t.Cell>{{user.name}}</t.Cell>
<t.Cell>{{user.email}}</t.Cell>
<t.Cell>
<Chip @color={{user.statusColor}} @size='sm'>
{{user.status}}
</Chip>
</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
</template>
}
Apply custom styling to specific table elements:
| Task Name | Estimated Value | Priority |
|---|---|---|
| Critical Server Issue | $1,000 |
High
|
| Feature Enhancement | $500 |
Medium
|
| Documentation Update | $100 |
Low
|
import Component from '@glimmer/component';
import { SimpleTable } from 'frontile';
import { Chip } from 'frontile';
import { hash } from '@ember/helper';
export default class DemoComponent extends Component {
items = [
{
name: 'Critical Server Issue',
value: '$1,000',
priority: 'High',
priorityColor: 'danger'
},
{
name: 'Feature Enhancement',
value: '$500',
priority: 'Medium',
priorityColor: 'warning'
},
{
name: 'Documentation Update',
value: '$100',
priority: 'Low',
priorityColor: 'success'
}
];
<template>
<div class='demo-stack demo-stack--wide'>
<SimpleTable
@classes={{hash
wrapper='border-2 border-primary-soft rounded-lg overflow-hidden'
table='border-separate border-spacing-0'
thead='bg-gradient-to-r from-primary-subtle to-primary-soft'
th='font-bold text-primary-strong border-b border-primary-soft'
tr='hover:bg-primary-subtle transition-colors'
td='border-b border-primary-subtle'
}}
as |t|
>
<t.Header>
<t.Column>Task Name</t.Column>
<t.Column>Estimated Value</t.Column>
<t.Column>Priority</t.Column>
</t.Header>
<t.Body>
{{#each this.items as |item|}}
<t.Row>
<t.Cell>{{item.name}}</t.Cell>
<t.Cell class='font-mono'>{{item.value}}</t.Cell>
<t.Cell>
<Chip @color={{item.priorityColor}} @size='sm'>
{{item.priority}}
</Chip>
</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
</template>
}
Add footers for summaries and totals:
| Order ID | Product | Quantity | Unit Price | Total |
|---|---|---|---|---|
| #001 | Laptop | 2 | $999.99 | $1999.98 |
| #002 | Mouse | 5 | $29.99 | $149.95 |
| #003 | Keyboard | 3 | $79.99 | $239.97 |
| Order Summary | 10 items | $2389.8999999999996 | ||
import Component from '@glimmer/component';
import { SimpleTable } from 'frontile';
export default class DemoComponent extends Component {
orders = [
{ id: '#001', product: 'Laptop', quantity: 2, price: 999.99 },
{ id: '#002', product: 'Mouse', quantity: 5, price: 29.99 },
{ id: '#003', product: 'Keyboard', quantity: 3, price: 79.99 }
];
get totalQuantity() {
return this.orders.reduce((sum, order) => sum + order.quantity, 0);
}
get totalValue() {
return this.orders.reduce(
(sum, order) => sum + order.quantity * order.price,
0
);
}
calculateTotal = (quantity, price) => (quantity * price).toFixed(2);
<template>
<div class='demo-stack demo-stack--wide'>
<SimpleTable as |t|>
<t.Header>
<t.Column>Order ID</t.Column>
<t.Column>Product</t.Column>
<t.Column>Quantity</t.Column>
<t.Column>Unit Price</t.Column>
<t.Column>Total</t.Column>
</t.Header>
<t.Body>
{{#each this.orders as |order|}}
<t.Row>
<t.Cell>{{order.id}}</t.Cell>
<t.Cell>{{order.product}}</t.Cell>
<t.Cell>{{order.quantity}}</t.Cell>
<t.Cell>${{order.price}}</t.Cell>
<t.Cell>${{this.calculateTotal
order.quantity
order.price
}}</t.Cell>
</t.Row>
{{/each}}
</t.Body>
<t.Footer>
<t.Column colspan='2' class='font-semibold'>Order Summary</t.Column>
<t.Column class='font-semibold'>{{this.totalQuantity}}
items</t.Column>
<t.Column />
<t.Column class='font-bold text-lg'>${{this.totalValue}}</t.Column>
</t.Footer>
</SimpleTable>
</div>
</template>
}
SimpleTable supports loading states with different color variants to indicate when data is being fetched or processed.
| ID | Product | Price | Category |
|---|---|---|---|
| 1 | Wireless Headphones | $199.99 | Electronics |
| 2 | Coffee Mug | $12.99 | Kitchen |
| 3 | Notebook Set | $24.99 | Office |
import Component from '@glimmer/component';
import { SimpleTable } from 'frontile';
import { Select } from 'frontile';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
import { Button } from 'frontile';
interface Product {
id: string;
name: string;
price: number;
category: string;
}
export default class DemoComponent extends Component {
@tracked isLoading = true;
@tracked loadingColor = 'primary';
items: Product[] = [
{
id: '1',
name: 'Wireless Headphones',
price: 199.99,
category: 'Electronics'
},
{ id: '2', name: 'Coffee Mug', price: 12.99, category: 'Kitchen' },
{ id: '3', name: 'Notebook Set', price: 24.99, category: 'Office' }
];
colorOptions = [
{ key: 'default', name: 'Default' },
{ key: 'primary', name: 'Primary' },
{ key: 'success', name: 'Success' },
{ key: 'warning', name: 'Warning' },
{ key: 'danger', name: 'Danger' }
];
@action
toggleLoading() {
this.isLoading = !this.isLoading;
}
@action
updateLoadingColor(color) {
this.loadingColor = color;
}
<template>
<div class='demo-stack demo-stack--wide'>
<div class='space-y-4'>
<div class='flex items-end space-x-4 justify-center'>
<Button
@onPress={{this.toggleLoading}}
@size='sm'
@variant='outline'
@color={{if this.isLoading 'danger' 'primary'}}
>
{{if this.isLoading 'Stop Loading' 'Start Loading'}}
</Button>
<Select
@inputSize='sm'
@label='Color'
@items={{this.colorOptions}}
@selectedKey={{this.loadingColor}}
@onSelectionChange={{this.updateLoadingColor}}
class='w-32'
/>
</div>
<SimpleTable
@isLoading={{this.isLoading}}
@loadingColor={{this.loadingColor}}
as |t|
>
<t.Header>
<t.Column>ID</t.Column>
<t.Column>Product</t.Column>
<t.Column>Price</t.Column>
<t.Column>Category</t.Column>
</t.Header>
<t.Body>
{{#each this.items as |item|}}
<t.Row>
<t.Cell>{{item.id}}</t.Cell>
<t.Cell>{{item.name}}</t.Cell>
<t.Cell>${{item.price}}</t.Cell>
<t.Cell>{{item.category}}</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</div>
</div>
</template>
}
The loading feature supports five color variants:
default - Standard gray loading animationprimary - Uses the primary theme colorsuccess - Green loading animation for success stateswarning - Orange/yellow loading animation for warningsdanger - Red loading animation for error statesSimpleTable renders real table markup — <table>, <thead>, <tbody>, <tfoot>, <tr>,
<th>, <td> — so screen readers get row and column structure, dimensions, and cell
navigation from the browser rather than from ARIA. That is the whole reason to reach for it
over a grid of divs.
Header cells carry scope="col", which associates a column's data cells with its header for
assistive technology. If you need a row header, put scope="row" on that cell yourself —
the ...attributes spread means it overrides the default.
Two things the component cannot do for you:
<caption> in the default block, or aria-labelledby on the table pointing at a heading.<td> is fine; a click handler on the <tr> itself is not reachable by keyboard.If the table scrolls (@isScrollable), the scroll container needs to be keyboard reachable
so someone can scroll it without a mouse — give it tabindex="0" and a label.
Element: HTMLTableElement
| Name | Type | Default | Description |
|---|---|---|---|
classes
|
SlotsToClasses<'table' | 'tbody' | 'td' | 'tfoot' | 'th' | 'thead' | 'tr' | 'separator' | 'wrapper' | 'empty' | 'toolbar' | 'sortButton' | 'sortIcon' | 'columnVisibilityButton' | 'columnVisibilityIcon' | 'skeleton' | 'skeletonRow'>
|
- | Custom CSS classes for different table elements (wrapper, table, th, td, etc.) |
hasCustomLoading
|
boolean
|
- | Whether a custom loading block is provided (disables CSS loading indicator) |
hasWrapper
|
boolean
|
true
|
Whether to render the wrapper div. |
isLoading
|
boolean
|
- | Enable loading state styling and behavior |
isScrollable
|
boolean
|
- | Enable scrolling for the table container |
isStriped
|
boolean
|
- | Enable striped rows (alternating background colors) |
layout
|
enum
|
'auto'
|
Table layout algorithm - 'auto' sizes columns by content, 'fixed' uses first row for sizing. |
loadingColor
|
enum
|
'default'
|
Color variant for loading animation. |
selectionColor
|
enum
|
'primary'
|
Color variant for selection highlight. |
size
|
enum
|
'md'
|
Size variant for table cells and headers. |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |
Element: HTMLTableSectionElement
| Name | Type | Default | Description |
|---|---|---|---|
class
|
string
|
- | Additional CSS class to apply to the header section |
isSticky
|
boolean
|
- | Whether the header should be sticky during vertical scrolling |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |
Element: HTMLTableSectionElement
| Name | Type | Default | Description |
|---|---|---|---|
class
|
string
|
- | Additional CSS class to apply to the body section |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |
Element: HTMLTableSectionElement
| Name | Type | Default | Description |
|---|---|---|---|
class
|
string
|
- | Additional CSS class to apply to the footer element |
isSticky
|
boolean
|
- | Whether this footer should be sticky during vertical scrolling |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |
Element: HTMLTableCellElement
| Name | Type | Default | Description |
|---|---|---|---|
class
|
string
|
- | Additional CSS class to apply to the header cell |
isSticky
|
boolean
|
- | Whether this column should be sticky during horizontal scrolling |
stickyPosition
|
enum
|
- | Position where the sticky column should stick. |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |
Element: HTMLTableRowElement
| Name | Type | Default | Description |
|---|---|---|---|
class
|
string
|
- | Additional CSS class to apply to the row |
hasStickyHeader
|
boolean
|
- | Whether the table has a sticky header (affects positioning of sticky rows) |
isSticky
|
boolean
|
- | Whether this row should be sticky during vertical scrolling |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |
Element: HTMLTableCellElement
| Name | Type | Default | Description |
|---|---|---|---|
class
|
string
|
- | Additional CSS class to apply to the cell |
isInStickyRow
|
boolean
|
- | Whether this cell is part of a sticky row (used for intersection styling) |
isSticky
|
boolean
|
- | Whether this cell should be sticky during horizontal scrolling |
stickyPosition
|
enum
|
- | Position where the sticky cell should stick. |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |