# 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` (and `@svgrid/grid`). Category: Base. Live demo: https://svgrid.com/demos/717-base-read-only-share/

Install: `npm i @svgrid/grid @svgrid/enterprise` (Svelte 5).

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

- [People, comments and files](https://svgrid.com/docs/help/base/collaboration.md)

## Related articles

- [A Fill Handle (Drag to Fill) in SvGrid](https://svgrid.com/blog/fill-handle-drag-to-fill/) - 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](https://svgrid.com/blog/svelte-data-grid-sveltekit-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](https://svgrid.com/blog/smart-ui-gold-award-2025/) - 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)

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

[View this example on GitHub](https://github.com/sv-grid/sv-grid/blob/main/examples/src/demos/717-base-read-only-share.svelte)
