Lattice Grid Buy a licence

demo D220

Open positions board in Angular

A recruitment dashboard screen: a progress bar, a status pill and a templated kebab, on a card

render: 'progress' · 'pill' · cell.template

This is a recruitment 'Open positions' board, the kind of screen a hiring team works in all day: each role shows how full its pipeline is as a bar, its state as a coloured status pill, and a row menu for actions like pause or close. It is the standard grid wearing your product's own look, taking its colours, spacing and type from the theme so it drops cleanly into a polished application.

Loading a live grid…

This is the Angular version. A standalone component takes the whole configuration through one config input, surfaces grid events as outputs, and exposes the live grid on a getter for anything the inputs do not cover.

The configuration

import { Component, ViewEncapsulation } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { LatticeGridComponent, provideLattice } from '@toclocoinc/lattice-grid/angular';

const jobs = [
  { id: '1', title: 'Senior Frontend Developer', dept: 'Engineering', fill: 72, location: 'San Francisco, CA', applications: 24, status: 'Open' },
  { id: '2', title: 'Product Manager', dept: 'Product', fill: 45, location: 'New York, NY', applications: 18, status: 'Open' },
  { id: '3', title: 'UX Designer', dept: 'Design', fill: 30, location: 'Austin, TX', applications: 31, status: 'Interviewing' },
  // ...
];

const columns = [
  { field: 'title', title: 'Job title', layout: { width: 280 }, cell: { class: 'is-strong' } },
  { field: 'fill', title: 'Department', sort: { enabled: false }, layout: { width: 230 },
    cell: { render: 'progress', props: { min: 0, max: 100, showValue: false, variant: 'amber', label: 'Pipeline filled' } } },
  { field: 'dept', title: ' ', sort: { enabled: false }, layout: { width: 140 }, header: { render: () => '' } },
  { field: 'location', title: 'Location', layout: { width: 180 } },
  { field: 'applications', title: 'Applications', type: 'number', total: 'sum', layout: { width: 130 } },
  { field: 'status', title: 'Status', layout: { width: 150 },
    cell: { render: 'pill', props: { shape: 'pill',
      variant: { map: { Open: 'success', Interviewing: 'info', Offer: 'accent', Paused: 'warning' }, default: 'neutral' } } } },
  { id: 'act', title: 'Action', sort: { enabled: false }, layout: { width: 90 },
    cell: { template: '<button class="kebab" aria-label="Row actions">\u22ee</button>' } },
];

// The card and the grid share one palette, set through the theme's real token
// names. ViewEncapsulation.None lets these reach the grid and the pill renderer.
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LatticeGridComponent],
  encapsulation: ViewEncapsulation.None,
  styles: [`
    :host { display: block; font-family: "IBM Plex Sans", system-ui, sans-serif; background: #EFEDE4; padding: 34px; }
    .card { background: #FBFAF5; border-radius: 20px; padding: 22px 24px 12px;
      box-shadow: 0 1px 2px rgba(20,22,28,.04), 0 18px 46px -30px rgba(20,22,28,.4); }
    .card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
    .card-head h2 { font-size: 1.25rem; font-weight: 700; margin: 0; }
    .lattice {
      --lattice-background: transparent;
      --lattice-border-color: #EDEAE0;
      --lattice-header-foreground: #8C8A80;
      --lattice-accent: #F0A94C;
      --lattice-variant-success-fill: #DEF7EC; --lattice-variant-success-text: #036C4F;
      --lattice-variant-info-fill: #E1EFFE;    --lattice-variant-info-text: #1A56DB;
      --lattice-variant-accent-fill: #EDE9FE;  --lattice-variant-accent-text: #6D28D9;
      --lattice-variant-warning-fill: #FEF0DC; --lattice-variant-warning-text: #92400E;
    }
    .lattice .lat-header-cell, .lattice .lat-cell { border-right: 0; }
    .lattice .is-strong { font-weight: 600; }
    .lattice [data-variant="amber"] .lat-progress-track {
      background-image: repeating-linear-gradient(-45deg, #E7EBF3 0 4px, #F4F6FA 4px 8px);
      background-color: #F4F6FA; border-radius: 999px; height: 12px;
    }
    .lattice [data-variant="amber"] .lat-progress-fill { background: #F5B863; border-radius: 999px; }
    .lattice .kebab { font: inherit; font-size: 18px; color: #8C8A80; background: none; border: 0; cursor: pointer; }
  `],
  template:
    '<div class="card">' +
    '  <div class="card-head"><h2>Open Job Positions</h2></div>' +
    '  <lattice-grid [config]="grid"></lattice-grid>' +
    '</div>',
})
export class AppComponent {
  grid = {
    rowKey: 'id',
    density: 'spacious',              // the generous row rhythm
    showColumnFunctions: false,      // quiet headers, no sort/filter/menu furniture
    autoHeight: true,
    columns,
    rows: jobs,
  };
}

bootstrapApplication(AppComponent, {
  providers: [provideLattice({ createGrid })],
});