Finance app
A personal and business ledger. Choose an account to update the balance, cash-flow chart and spending breakdown. Search transactions, filter money in or out, and compare two months of demo data. Changes stay in this example and reset when you reload.
Point at any part of the page to see what it is made of.
- Page
ds-page- Unit
ds-row,ds-sidebar,ds-table
<style>
.example-stack { display:grid; grid-template-columns:minmax(0,1fr); gap:var(--ds-unit); min-width:0; align-content:start; }
.example-tight { display:grid; grid-template-columns:minmax(0,1fr); gap:calc(var(--ds-unit) / 2); min-width:0; align-content:start; }
.example-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--ds-unit); }
.example-two { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:calc(2 * var(--ds-unit)); align-items:start; }
.example-actions { display:flex; flex-wrap:wrap; align-items:flex-end; gap:calc(var(--ds-unit) / 2); }
.example-between { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:flex-end; gap:var(--ds-unit); }
.example-grow { flex:1; min-width:0; }
.example-section { padding-block:var(--ds-unit); }
.example-between > ds-input { width:calc(10 * var(--ds-unit)); max-width:100%; }
.example-overflow { overflow-x:auto; }
.example-rule { border-top:1px solid var(--ds-border-color); padding-top:var(--ds-unit); }
.example-caption { display:flex; justify-content:space-between; gap:var(--ds-unit); }
.example-caption > * { min-width:0; }
.example-nav { display:flex; justify-content:space-between; align-items:flex-end; gap:var(--ds-unit); }
.example-brand { flex:none; }
.example-footer { padding-top:var(--ds-unit); }
.example-stack [hidden], .example-grid [hidden], .example-two [hidden], .example-tight [hidden] { display:none!important; }
.example-status:empty { display:none; }
.example-title { --ds-sf:calc(2 * var(--sf, 1)); --ds-display-text-size:40px; --ds-typeface-regular:var(--typeface-compressed); }
.example-copy { max-width:calc(24 * var(--ds-unit)); }
.example-shop-hero { display:grid; grid-template-columns:2fr 3fr; gap:var(--ds-unit); align-items:stretch; }
.example-shop-intro { display:flex; flex-direction:column; justify-content:space-between; gap:calc(2 * var(--ds-unit)); padding-block:var(--ds-unit); }
.example-shop-hero ds-image { align-self:start; }
.example-story { display:grid; grid-template-columns:1fr 2fr; gap:calc(2 * var(--ds-unit)); padding-block:calc(2 * var(--ds-unit)); }
.example-product { display:grid; gap:calc(var(--ds-unit) / 2); min-width:0; }
.example-bag { padding-block:var(--ds-unit); border-block:1px solid var(--ds-border-color); }
.example-workspace { --ds-page-nav:calc(8 * var(--ds-unit)); --ds-page-gap:calc(2 * var(--ds-unit)); }
.example-workspace-nav { display:grid; gap:calc(2 * var(--ds-unit)); }
.example-week { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:calc(var(--ds-unit) / 2); }
.example-day { display:grid; gap:calc(var(--ds-unit) / 2); min-height:calc(4 * var(--ds-unit)); padding-block:calc(var(--ds-unit) / 2); border-top:1px solid var(--ds-border-color); }
.example-day[aria-current] { border-top-color:var(--ds-text-color); }
.example-work { display:grid; grid-template-columns:minmax(0,3fr) minmax(0,2fr); gap:calc(2 * var(--ds-unit)); }
.example-brief { display:grid; gap:calc(var(--ds-unit) / 2); }
.example-contact-sheet { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:calc(var(--ds-unit) / 2); }
.example-contact-sheet > :first-child { grid-column:1 / -1; }
.example-item { box-sizing:border-box; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:calc(var(--ds-unit) / 2); min-height:var(--ds-unit); align-items:end; border-bottom:1px solid var(--ds-border-color); }
.example-item label { display:flex; align-items:flex-end; gap:calc(var(--ds-unit) / 2); min-width:0; }
.example-item label ds-text { flex:1; }
.example-item-meta { display:flex; align-items:flex-end; gap:calc(var(--ds-unit) / 2); }
#balance-stat { --ds-stat-size:40px; --ds-stat-lines:3; --ds-stat-typeface:var(--typeface-compressed); }
.example-stats { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--ds-unit); }
.example-cash { display:grid; grid-template-columns:minmax(0,3fr) minmax(0,1fr); gap:calc(2 * var(--ds-unit)); }
.example-goal { display:grid; grid-template-columns:calc(6 * var(--ds-unit)) minmax(0,1fr); gap:var(--ds-unit); align-items:center; }
.example-audio-hero { display:grid; grid-template-columns:1fr 1fr; gap:var(--ds-unit); }
.example-audio-copy { display:flex; flex-direction:column; justify-content:space-between; gap:var(--ds-unit); padding:var(--ds-unit); background:var(--one-pink-pastel); }
.example-audio-copy .example-title { --ds-typeface-regular:var(--typeface-regular); }
.example-audio-strip { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--ds-unit); padding-block:var(--ds-unit); }
.example-audio-detail { display:grid; grid-template-columns:1fr 1fr; gap:calc(2 * var(--ds-unit)); align-items:center; }
.example-package { display:grid; grid-template-columns:1fr 1fr; gap:calc(2 * var(--ds-unit)); padding-block:calc(2 * var(--ds-unit)); }
.example-specs { display:grid; gap:0; }
.example-specs > div { display:flex; justify-content:space-between; gap:var(--ds-unit); border-bottom:1px solid var(--ds-border-color); padding-block:calc(var(--ds-unit) / 2); }
@media(max-width:900px) { .example-title { --ds-sf:var(--sf, 1); } .example-work { grid-template-columns:1fr; } .example-brief { grid-template-columns:1fr 1fr; gap:var(--ds-unit); } .example-cash { grid-template-columns:minmax(0,1fr); } }
@media(max-width:600px) {
.example-title { --ds-sf:var(--sf, 1); --ds-display-text-size:30px; }
.example-shop-hero,.example-audio-hero,.example-audio-detail,.example-package,.example-two,.example-story,.example-brief { grid-template-columns:1fr; gap:var(--ds-unit); }
.example-shop-intro { padding-block:0; gap:var(--ds-unit); }
.example-shop-hero { --hero-ratio:4 / 3; }
.example-grid { grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--ds-unit) calc(var(--ds-unit) / 2); }
.example-grid > :first-child { grid-column:1 / -1; }
.example-stats { grid-template-columns:repeat(2,minmax(0,1fr)); }
.example-stats > :first-child { grid-column:1 / -1; }
.example-audio-strip { grid-template-columns:1fr; }
.example-week { gap:calc(var(--ds-unit) / 2); }
.example-day { min-height:calc(3 * var(--ds-unit)); }
.example-day ds-text[size=small] { display:none; }
.example-nav .example-nav-secondary { display:none; }
.example-goal { grid-template-columns:calc(4 * var(--ds-unit)) minmax(0,1fr); }
.example-item { grid-template-columns:1fr; }
.example-item-meta { padding-inline-start:calc(1.5 * var(--ds-unit)); }
.example-package { padding-block:var(--ds-unit); }
}
</style><ds-page layout="sidebar" drawer divided fill class="example-workspace" data-ds-surface>
<div slot="nav" class="example-workspace-nav"><ds-row><ds-avatar initial="L" color="var(--one-comfortable-green)"></ds-avatar><ds-text literal strong>Ledger</ds-text></ds-row><ds-sidebar label="Accounts" id="finance-nav"></ds-sidebar><div class="example-tight"><ds-text literal size="small" tone="muted">Your next chapter</ds-text><ds-image src="/examples/images/lake.jpg" alt="A mountain lake, the destination of a savings goal" ratio="4 / 3" ></ds-image><ds-text literal strong>A week away</ds-text><ds-text literal size="small" wrap>€2,400 of €3,000 saved</ds-text><ds-progress value="0.8" label="80% of the savings goal"></ds-progress></div></div>
<div slot="header" class="example-between"><ds-text literal strong>Overview</ds-text><div class="example-actions"><ds-tag>Demo data</ds-tag><ds-button id="month" secondary>October</ds-button></div></div>
<div class="example-stack">
<div class="example-stats"><ds-stat id="balance-stat" label="Available balance" value="18420" format="money" currency="EUR" whole caption="Across two accounts" literal></ds-stat><ds-stat id="income-stat" label="Money in" value="6200" format="money" currency="EUR" whole caption="2 client payments" literal></ds-stat><ds-stat id="spent-stat" label="Money out" value="408.8" format="money" currency="EUR" caption="3 transactions" literal></ds-stat></div>
<section class="example-cash example-rule"><div class="example-stack"><div class="example-between"><ds-text literal strong>Balance through the month</ds-text><ds-text literal size="small" tone="muted" id="chart-period">1–19 October</ds-text></div><ds-chart id="cash-chart" kind="area" rows="10" color="--one-sharp-blue" labels aria-label="Daily available balance"></ds-chart><div class="example-caption"><ds-text literal size="small" tone="muted" id="chart-start">1 Oct</ds-text><ds-text literal size="small" tone="muted" id="chart-end">19 Oct</ds-text></div></div><div class="example-stack"><ds-text literal strong>Where it went</ds-text><div class="example-tight"><div class="example-caption"><ds-text literal size="small">Production</ds-text><ds-text literal size="small" id="production-spent">€280</ds-text></div><ds-progress id="production-ratio" value="0.685" label="Production spending"></ds-progress><div class="example-caption"><ds-text literal size="small">Travel</ds-text><ds-text literal size="small" id="travel-spent">€86</ds-text></div><ds-progress id="travel-ratio" value="0.21" label="Travel spending"></ds-progress><div class="example-caption"><ds-text literal size="small">Groceries</ds-text><ds-text literal size="small" id="groceries-spent">€42.80</ds-text></div><ds-progress id="groceries-ratio" value="0.105" label="Grocery spending"></ds-progress></div><ds-text literal size="small" tone="muted" wrap>Every outgoing payment, in one place.</ds-text></div></section>
<section class="example-stack example-rule"><div class="example-between"><ds-text literal size="display" strong wrap role="heading" aria-level="2">Recent activity</ds-text><ds-input id="transaction-search" type="search" label="Find a transaction" placeholder="Find a transaction"></ds-input></div><div class="example-actions" role="group" aria-label="Transaction type"><ds-button control="host" active data-filter="all" aria-pressed="true">All</ds-button><ds-button control="host" data-filter="income" aria-pressed="false">Money in</ds-button><ds-button control="host" data-filter="expense" aria-pressed="false">Money out</ds-button></div><div class="example-overflow"><ds-table id="transactions" label="Transactions" sticky></ds-table></div><ds-text id="transaction-count" literal role="status" tone="muted" size="small"></ds-text></section>
<section class="example-goal example-rule"><ds-image src="/examples/images/lake.jpg" alt="Mountains reflected in a still lake" ratio="4 / 3" ></ds-image><div class="example-tight"><ds-text literal strong wrap>A little closer to the mountains.</ds-text><ds-text literal wrap tone="muted">Your next €600 completes the travel fund.</ds-text><ds-progress value="0.8" label="Travel fund: €2,400 of €3,000"></ds-progress></div></section>
</div><div slot="footer" class="example-footer"><ds-text literal size="small" tone="muted" wrap>Fictional demo. Your changes stay here.</ds-text></div></ds-page> const nav = document.querySelector('#finance-nav'); let account='all', filter='all', september=false;
const entries=[{date:'19 Oct',name:'North studio',category:'Client payment',amount:3200,account:'business'},{date:'18 Oct',name:'Corner market',category:'Groceries',amount:-42.80,account:'personal'},{date:'17 Oct',name:'Rail ticket',category:'Travel',amount:-86,account:'personal'},{date:'16 Oct',name:'Print workshop',category:'Production',amount:-280,account:'business'},{date:'15 Oct',name:'Oak house',category:'Client payment',amount:3000,account:'business'}];
const chart=document.querySelector('#cash-chart');
new ResizeObserver(()=>{chart.unit=20*(parseFloat(getComputedStyle(chart).getPropertyValue('--ds-sf'))||1);}).observe(chart);
chart.format=value=>'€'+Math.round(value).toLocaleString('en-IE');
const table=document.querySelector('#transactions'); table.columns=[{key:'date',label:'Date'},{key:'name',label:'Name'},{key:'category',label:'Category'},{key:'amount',label:'Amount',numeric:true}];
function render() {
nav.items=[{id:'all',label:'All accounts',current:account==='all'},{id:'personal',label:'Personal',current:account==='personal'},{id:'business',label:'Business',current:account==='business'}];
const query=document.querySelector('#transaction-search').value.toLowerCase();
table.data=entries.filter(row => (account==='all'||row.account===account)&&(filter==='all'||(filter==='income'?row.amount>0:row.amount<0))&&(row.name+' '+row.category).toLowerCase().includes(query)).map(row=>({...row,date:row.date.replace('Oct',september?'Sep':'Oct'),amount:new Intl.NumberFormat('en-IE',{style:'currency',currency:'EUR'}).format(row.amount*(september?0.8:1))}));
document.querySelector('#transaction-count').textContent=table.data.length+' transactions';
document.querySelector('#month').textContent=september?'September':'October';
const selected=entries.filter(row=>account==='all'||row.account===account);const factor=september?0.8:1;const income=selected.reduce((sum,row)=>sum+Math.max(0,row.amount),0)*factor;const spent=selected.reduce((sum,row)=>sum+Math.max(0,-row.amount),0)*factor;const opening=(account==='personal'?4428.8:account==='business'?8200:12628.8)*factor;
document.querySelector('#income-stat').value=income;document.querySelector('#spent-stat').value=spent;document.querySelector('#balance-stat').value=opening+income-spent;document.querySelector('#balance-stat').caption=account==='all'?'Across two accounts':account==='personal'?'Personal account':'Business account';
document.querySelector('#income-stat').caption=selected.filter(row=>row.amount>0).length+' payments';document.querySelector('#spent-stat').caption=selected.filter(row=>row.amount<0).length+' transactions';
for(const [id,category] of [['production','Production'],['travel','Travel'],['groceries','Groceries']]){const amount=Math.max(0,-selected.filter(row=>row.category===category).reduce((sum,row)=>sum+row.amount,0)*factor);document.querySelector('#'+id+'-spent').textContent=new Intl.NumberFormat('en-IE',{style:'currency',currency:'EUR'}).format(amount);document.querySelector('#'+id+'-ratio').value=spent?amount/spent:0;}
const month=september?'Sep':'Oct';document.querySelector('#chart-period').textContent='1–19 '+(september?'September':'October');document.querySelector('#chart-start').textContent='1 '+month;document.querySelector('#chart-end').textContent='19 '+month;
document.querySelector('#cash-chart').timeRange=[0,18*86400];
document.querySelector('#cash-chart').data=Array.from({length:19},(_,index)=>({time:index*86400,value:opening+selected.filter(row=>Number(row.date.split(' ')[0])<=index+1).reduce((sum,row)=>sum+row.amount,0)*factor}));
}
nav.addEventListener('sidebar-activate', event=>{account=event.detail.id;render();document.querySelector('ds-page').closeDrawer();});
document.querySelector('#month').addEventListener('click',()=>{september=!september;render();});
document.querySelector('#transaction-search').addEventListener('input',render);
document.querySelectorAll('[data-filter]').forEach(button=>button.addEventListener('click',()=>{filter=button.dataset.filter;document.querySelectorAll('[data-filter]').forEach(item=>{item.toggleAttribute('active',item===button);item.setAttribute('aria-pressed',String(item===button));});render();}));
render(); The page uses the current DS1 components and tokens. The example only supplies content, layout composition and local interactions, so shared component updates carry through. Use Code to copy the example, Phone to check the narrow layout, or Show layers to inspect the pieces.