Read-only share
The same base as the team edits it and as a client sees it through a share link. readOnly turns off every way to change it: cells, records, card and calendar drags, the record and field menus, import, trash, undo and the comment box. Viewers still switch tables and views, search, sort and filter, and nothing they change reaches onTablesChange. A share can hide fields, and the formulas and rollups that read them go too. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Base). See the SvGrid documentation for the full API.
About this example
A read-only share of an SvGrid Base, in Svelte 5. The readOnly prop shows the same tables to people who may look but not change anything: no cell, record, card or calendar edits, no adding or deleting, no import, trash or undo, comments without the box to write one. Viewers still switch tables and views, search, filter, sort and open records, and their view changes stay in the page.
The same tables twice: as the team edits them, and as a client sees them through a share link. readOnly turns every way to change the base off: cells, the record view, card drags, calendar drags, the record and field menus, "+ Add", the tab bar's table menu, import, trash, undo and the comment box. A form view is not shared.
The client can still switch tables and views, search, filter, sort, group and open records. What they change in a view stays in their page; the app's onTablesChange never hears of it.
Comments are kept by table id and record id, not by base, so a comment the team writes shows in the client's link too (read, with no box to answer). An app that saves comments hands its store to setRecordComments, and both read from that.
readOnly={{ hideFields: ['budget'] }} leaves Budget out, and with it the fields that would show it: the rollup of it on Clients and the formula that reads it. Hiding is for display; a share of something secret is a copy the server builds without it.
Imports, features and API used
Imports: @svgrid/grid, @svgrid/enterprise/base, ../shared/base-people
Columns: name (Project), client (Client), status (Status), lead (Lead), start (Start), end (Due), budget (Budget), weekly (Per week), status, status, name (Client), contact (Contact)
Frequently asked questions
What does readOnly turn off?
Editing cells and records, dragging cards and calendar events, adding and deleting records, fields and tables, the field editor, import, the trash, undo and redo, automations and writing comments. Form views are not shared, and the selection bar loses Edit fields and Delete.
Do a viewer's sorts and filters get saved?
No. A view a viewer changes keeps the change while the page is open, and switching tables keeps it too, but nothing reaches onTablesChange or a table's database source.
Can a share show only some tables, views or fields?
Yes: readOnly={{ tables, views, hideFields }} takes lists of ids. A hidden field takes along the fields that would show it, such as a formula or a rollup that reads it. Hiding is for display, so a share of secret data should be a copy the server builds without it.
Which license does it need?
SvGrid Base is part of the Suite edition of @svgrid/enterprise. Without a key it runs with a watermark, so it can be evaluated as is.
Related documentation
Related articles
- A Fill Handle (Drag to Fill) in SvGrid - Build a working spreadsheet-style fill handle on top of SvGrid's cell selection and editing - pointer tracking, range highlighting, series fill, and undo/redo integration all covered.
- A Svelte Data Grid with SvelteKit and Supabase - Wire SvGrid to a Supabase Postgres backend with server-side pagination, sorting, and filtering - keeping credentials on the server and queries fast with proper indexing.
- Smart UI Wins Gold in the 2025 Visual Studio Magazine Readers' Choice Awards - Smart UI, the web-components suite from the team behind SvGrid, earned Gold in the 2025 Visual Studio Magazine Readers' Choice Awards. Here is what that vote actually tells you about the codebase you are running.
Source code (717-base-read-only-share.svelte)
<script lang="ts">
/**
* 717. SvGrid Base: a read-only share
* -----------------------------------
* The same tables twice: as the team edits them, and as a client sees them
* through a share link. `readOnly` turns every way to change the base off:
* cells, the record view, card drags, calendar drags, the record and field
* menus, "+ Add", the tab bar's table menu, import, trash, undo and the
* comment box. A form view is not shared.
*
* The client can still switch tables and views, search, filter, sort,
* group and open records. What they change in a view stays in their page;
* the app's `onTablesChange` never hears of it.
*
* Comments are kept by table id and record id, not by base, so a comment
* the team writes shows in the client's link too (read, with no box to
* answer). An app that saves comments hands its store to
* `setRecordComments`, and both read from that.
*
* `readOnly={{ hideFields: ['budget'] }}` leaves Budget out, and with it
* the fields that would show it: the rollup of it on Clients and the
* formula that reads it. Hiding is for display; a share of something
* secret is a copy the server builds without it.
*/
import { SvToaster } from '@svgrid/grid'
import { SvBase, enableBase, type BaseTable } from '@svgrid/enterprise/base'
import { usePeople } from '../shared/base-people'
enableBase()
usePeople()
const STATUS = [
{ value: 'Planned', color: '#94a3b8' },
{ value: 'In progress', color: '#2d7ff9' },
{ value: 'Review', color: '#ff9f0a' },
{ value: 'Done', color: '#20c933' },
]
const PROJECTS: Array<[string, string, string, string, number, string, string]> = [
['Spring campaign site', 'c1', 'In progress', 'u1', 18000, '2026-11-04', '2026-11-21'],
['Product photo shoot', 'c1', 'Planned', 'u3', 6500, '2026-11-12', '2026-11-14'],
['Checkout redesign', 'c2', 'Review', 'u2', 24000, '2026-10-27', '2026-11-10'],
['Loyalty app onboarding', 'c2', 'In progress', 'u5', 14500, '2026-11-02', '2026-11-28'],
['Annual report layout', 'c3', 'Done', 'u6', 9000, '2026-10-06', '2026-10-31'],
['Brand guidelines refresh', 'c3', 'Planned', 'u4', 11000, '2026-11-17', '2026-12-05'],
]
const projects = PROJECTS.map(([name, client, status, lead, budget, start, end], i) => ({ id: `pr${i + 1}`, name, client: [client], status, lead, budget, start, end }))
const clients = [
{ id: 'c1', name: 'Northwind Outdoors', contact: 'Ana Ruiz' },
{ id: 'c2', name: 'Fabrikam Retail', contact: 'Ben Okafor' },
{ id: 'c3', name: 'Litware Foundation', contact: 'Farah Haddad' },
].map((c) => ({ ...c, projects: projects.filter((p) => p.client[0] === c.id).map((p) => p.id) }))
// The team's tables. Edit them, then look at the shared link: it shows the change.
let tables = $state.raw<BaseTable[]>([
{
id: 'share-projects',
name: 'Projects',
primaryField: 'name',
data: projects,
columns: [
{ field: 'name', header: 'Project', width: 230, fieldType: 'text' },
{ field: 'client', header: 'Client', width: 180, fieldType: 'link', fieldProps: { table: 'share-clients', multiple: false, inverse: 'projects' } },
{ field: 'status', header: 'Status', width: 130, fieldType: 'singleSelect', fieldProps: { options: STATUS } },
{ field: 'lead', header: 'Lead', width: 150, fieldType: 'user' },
{ field: 'start', header: 'Start', width: 120, fieldType: 'date', fieldProps: { dateFormat: 'friendly' } },
{ field: 'end', header: 'Due', width: 120, fieldType: 'date', fieldProps: { dateFormat: 'friendly' } },
{ field: 'budget', header: 'Budget', width: 120, fieldType: 'currency', fieldProps: { precision: 0 } },
{ field: 'weekly', header: 'Per week', width: 120, fieldType: 'formula', fieldProps: { formula: 'ROUND({Budget} / 4, 0)' } },
],
views: [
{ id: 'all', name: 'All projects', type: 'grid', color: { mode: 'field', field: 'status' } },
{ id: 'board', name: 'By status', type: 'kanban', stackBy: 'status' },
{ id: 'calendar', name: 'Schedule', type: 'calendar', dateField: 'start', endField: 'end', color: { mode: 'field', field: 'status' } },
{ id: 'brief', name: 'New project brief', type: 'form' },
],
},
{
id: 'share-clients',
name: 'Clients',
primaryField: 'name',
data: clients,
columns: [
{ field: 'name', header: 'Client', width: 210, fieldType: 'text' },
{ field: 'contact', header: 'Contact', width: 160, fieldType: 'text' },
{ field: 'projects', header: 'Projects', width: 320, fieldType: 'link', fieldProps: { table: 'share-projects', multiple: true, inverse: 'client' } },
{ field: 'total', header: 'Total budget', width: 140, fieldType: 'rollup', fieldProps: { linkField: 'projects', field: 'budget', aggregation: 'SUM' } },
],
views: [{ id: 'all', name: 'All clients', type: 'grid' }],
},
])
let mode = $state<'edit' | 'share'>('share')
let hideBudget = $state(true)
// The app's own record of saves: a client's view changes never land here.
let saves = $state(0)
</script>
<section class="share-demo">
<div class="share-demo-bar">
<div class="share-demo-switch" role="group" aria-label="Show the base as">
<button type="button" aria-pressed={mode === 'edit'} onclick={() => (mode = 'edit')}>Team (editing)</button>
<button type="button" aria-pressed={mode === 'share'} onclick={() => (mode = 'share')}>Client (share link)</button>
</div>
{#if mode === 'share'}
<label class="share-demo-check"><input type="checkbox" bind:checked={hideBudget} /> Hide Budget from the client</label>
{/if}
<span class="share-demo-saves" aria-live="polite">Share link onTablesChange: {saves} {saves === 1 ? 'call' : 'calls'}</span>
</div>
<p class="share-demo-tip">
{#if mode === 'share'}
Try to change something: double-click a cell, drag a card on By status, right-click a record. Sorting, filtering and switching views work, and stay in this page.
{:else}
Edit a status or a date here, or open a record and comment on it, then switch to the client's link: it shows the change and the comment.
{/if}
</p>
{#key mode === 'share' ? `share-${hideBudget}` : 'edit'}
{#if mode === 'share'}
<SvBase
{tables}
readOnly={hideBudget ? { hideFields: ['budget'] } : true}
onTablesChange={() => (saves += 1)}
sortable
showRowNumbers
expandRows
rowNumberWidth={64}
showRowSelection={false}
rowHeight={40}
containerHeight="100%"
fieldMenu
/>
{:else}
<SvBase
bind:tables
editable
sortable
showRowNumbers
expandRows
rowNumberWidth={64}
showRowSelection={false}
rowHeight={40}
containerHeight="100%"
columnAdder
fieldMenu
/>
{/if}
{/key}
<SvToaster />
</section>
<style>
.share-demo { display: flex; flex-direction: column; flex: 1; min-height: 520px; height: 100%; gap: 10px; }
.share-demo-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; font-size: 13px; color: var(--sg-fg, #0f172a); }
.share-demo-switch { display: inline-flex; padding: 2px; border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius, 6px); background: var(--sg-muted-bg, var(--sg-header-bg, #f8fafc)); }
.share-demo-switch button {
height: 28px; padding: 0 12px; border: 0; border-radius: calc(var(--sg-radius, 6px) - 2px);
background: none; color: var(--sg-muted, #64748b); font: inherit; cursor: pointer;
}
.share-demo-switch button[aria-pressed='true'] { background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); font-weight: 600; box-shadow: 0 0 0 1px var(--sg-border, #e2e8f0); }
.share-demo-switch button:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #2563eb)); outline-offset: -2px; }
.share-demo-check { display: inline-flex; align-items: center; gap: 6px; }
.share-demo-check input { accent-color: var(--sg-accent, #2563eb); }
.share-demo-saves { margin-left: auto; font-size: 12px; color: var(--sg-muted, #64748b); font-variant-numeric: tabular-nums; }
.share-demo-tip { margin: 0; font-size: 13px; line-height: 1.5; color: var(--sg-muted, #64748b); }
</style>More Base examples
- Project tracker - A project tracker, ready to use: Tasks and Milestones as two linked tables. Tasks have status, tags, assignee, dates, estimate, progress, budget and files; each milestone lists its tasks with their progress and budget. Views for all tasks, the board by status, due dates on a calendar, a gallery and a request form. Automations close finished tasks, start work from a button and flag overdue ones.
- Content calendar - An editorial content calendar, ready to use: Posts and Campaigns as two linked tables. Posts have stage, channels, author, publish date, cover and brief. Views for the plan, the pipeline as a Kanban, the publishing calendar, a wall of covers and a pitch form.
- Sales CRM - A sales CRM, ready to use: Deals, Accounts and Contacts as three linked tables. Link a deal to an account and the account lists it. Views for all deals, the pipeline as a Kanban, the closing calendar, open deals by owner and a new-lead form.
- Product roadmap - A product roadmap, ready to use: Features and customer Feedback as two linked tables. Features have quarter, status, teams, lead, impact, effort and votes. Views for the roadmap grouped by quarter, a status board, a timeline, the work in progress now and a request form.
- Hiring tracker - A hiring tracker, ready to use: Candidates and Roles as two linked tables. Candidates have photo, role, stage, recruiter, rating, interview date and resume; each role lists its candidates. Views for the pipeline as a Kanban, all candidates, a gallery of faces, interviews and an application form.