The SimpleTable component provides a foundational HTML table structure with consistent styling and theming support. It's designed for manual composition when you need complete control over your table layout without the advanced features of the Table component.
Key Features:
For automatic rendering with advanced features like 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>
<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>
</template>
}
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',
chipIntent: 'success'
},
{
id: '2',
name: 'Jane Smith',
email: 'jane@example.com',
status: 'Inactive',
chipIntent: 'default'
},
{
id: '3',
name: 'Bob Wilson',
email: 'bob@example.com',
status: 'Pending',
chipIntent: 'warning'
}
];
<template>
<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 @intent={{user.chipIntent}} @size='sm'>
{{user.status}}
</Chip>
</t.Cell>
<t.Cell>
<Button @variant='link' @size='sm'>
Edit
</Button>
</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</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',
stockChipIntent: 'success'
},
{
id: '2',
name: 'Smart Watch',
price: 299.99,
stock: 3,
stockStatus: 'Low Stock',
stockChipIntent: 'warning'
},
{
id: '3',
name: 'Bluetooth Speaker',
price: 59.99,
stock: 0,
stockStatus: 'Out of Stock',
stockChipIntent: 'danger'
}
];
<template>
<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 @intent={{product.stockChipIntent}} @size='sm'>
{{product.stockStatus}}
</Chip>
</div>
</t.Cell>
<t.Cell>
<div class='flex gap-2'>
<Button @intent='primary' @size='sm'>
Edit
</Button>
<Button @intent='danger' @size='sm'>
Delete
</Button>
</div>
</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</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='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>
</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='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>
</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',
statusIntent: 'success'
},
{
name: 'Jane Smith',
email: 'jane@example.com',
status: 'Active',
statusIntent: 'success'
},
{
name: 'Bob Johnson',
email: 'bob@example.com',
status: 'Inactive',
statusIntent: 'default'
},
{
name: 'Alice Brown',
email: 'alice@example.com',
status: 'Active',
statusIntent: 'success'
}
];
<template>
<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 @intent={{user.statusIntent}} @size='sm'>
{{user.status}}
</Chip>
</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</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',
priorityIntent: 'danger'
},
{
name: 'Feature Enhancement',
value: '$500',
priority: 'Medium',
priorityIntent: 'warning'
},
{
name: 'Documentation Update',
value: '$100',
priority: 'Low',
priorityIntent: 'success'
}
];
<template>
<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 @intent={{item.priorityIntent}} @size='sm'>
{{item.priority}}
</Chip>
</t.Cell>
</t.Row>
{{/each}}
</t.Body>
</SimpleTable>
</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>
<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>
</template>
}
The SimpleTable component supports loading states with different color variants to indicate when data is being fetched or processed. Loading states provide visual feedback to users during async operations.
| 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='space-y-4'>
<div class='flex items-end space-x-4 justify-center'>
<Button
@onPress={{this.toggleLoading}}
@size='sm'
@appearance='outlined'
@intent={{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>
</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: <span class="hljs-title class_">HTMLTableElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
classes
|
SlotsToClasses<'base' | 'table' | 'tbody' | 'td' | 'tfoot' | 'th' | 'thead' | 'tr' | 'separator' | 'wrapper' | 'sortButton' | 'sortIcon' | 'columnVisibilityButton' | 'columnVisibilityIcon' | 'empty' | '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: <span class="hljs-title class_">HTMLTableSectionElement</span>
| 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: <span class="hljs-title class_">HTMLTableSectionElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
class
|
string
|
- | Additional CSS class to apply to the body section |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |
Element: <span class="hljs-title class_">HTMLTableSectionElement</span>
| 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: <span class="hljs-title class_">HTMLTableCellElement</span>
| 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: <span class="hljs-title class_">HTMLTableRowElement</span>
| 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: <span class="hljs-title class_">HTMLTableCellElement</span>
| 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
|
- |