GitHub

SimpleTable

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:

  • Manual composition with block-form yielding
  • Flexible styling with size variants and striped rows
  • Consistent theming with the Frontile design system
  • Sticky support when used with the Table component
  • Lightweight - no data management or complex behaviors

For automatic rendering with advanced features like sticky elements and data management, use Table instead.

Import

import { SimpleTable } from 'frontile';

Usage

SimpleTable uses block form composition where you manually define the table structure:

ID Name Email 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>
}

Advanced Composition

Custom Headers

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>
}

Custom Cell Content

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>
}

Styling & Layout

Size Variants

Control table spacing with size variants:

Small (sm) - Compact spacing

Name Role
John Doe Developer
Jane Smith Designer

Medium (md) - Default spacing

Name Role
John Doe Developer
Jane Smith Designer

Large (lg) - Spacious layout

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>
}

Layout Options

Control column sizing behavior:

Auto Layout (default) - Content-based sizing

ID Name Email Department
1 John Doe john.doe.longname@example-company.com Engineering
2 Jane jane@ex.co Design

Fixed Layout - Equal column widths

ID Name Email 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>
}

Striped Rows

Enable alternating row colors for better readability:

Name Email 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>
}

Custom Classes

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>
}

Table Footers

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>
}

Loading State

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 animation
  • primary - Uses the primary theme color
  • success - Green loading animation for success states
  • warning - Orange/yellow loading animation for warnings
  • danger - Red loading animation for error states

Accessibility

SimpleTable 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:

  • Give the table an accessible name when the surrounding page doesn't already. Add a <caption> in the default block, or aria-labelledby on the table pointing at a heading.
  • Keep the markup semantic if you nest interactive content in cells. A button inside a <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.

API

SimpleTable

Element: <span class="hljs-title class_">HTMLTableElement</span>

Arguments

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.

Blocks

Name Type Default Description
default * Array -

SimpleTableHeader

Element: <span class="hljs-title class_">HTMLTableSectionElement</span>

Arguments

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

Blocks

Name Type Default Description
default * Array -

SimpleTableBody

Element: <span class="hljs-title class_">HTMLTableSectionElement</span>

Arguments

Name Type Default Description
class string - Additional CSS class to apply to the body section

Blocks

Name Type Default Description
default * Array -

SimpleTableFooter

Element: <span class="hljs-title class_">HTMLTableSectionElement</span>

Arguments

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

Blocks

Name Type Default Description
default * Array -

SimpleTableColumn

Element: <span class="hljs-title class_">HTMLTableCellElement</span>

Arguments

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.

Blocks

Name Type Default Description
default * Array -

SimpleTableRow

Element: <span class="hljs-title class_">HTMLTableRowElement</span>

Arguments

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

Blocks

Name Type Default Description
default * Array -

SimpleTableCell

Element: <span class="hljs-title class_">HTMLTableCellElement</span>

Arguments

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.

Blocks

Name Type Default Description
default * Array -
Released under MIT License - Created by Josemar Luedke