This guide explains how to create custom panes that integrate with the Solid data browser.
Panes are UI components that render specific types of Linked Data. The pane registry matches content to panes using RDF type checks.
┌─────────────────────────────────────────────────────────┐
│ Pane Registry │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ profile │ │ folder │ │ chat │ │ custom │ ... │
│ │ pane │ │ pane │ │ pane │ │ pane │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ label() - sync type check │ │
│ │ "Is this pane relevant to subject?" │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ render() - async lazy load │ │
│ │ "Load chunk and render the UI" │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
interface Pane {
// Required
name: string // Unique pane identifier
icon: string // Icon URL for pane selector
label: (subject, context) => string | null // Sync relevance check
render: (subject, context, options?) => HTMLElement | Promise<HTMLElement>
// Optional
audience?: NamedNode[] // e.g., [ns.solid('PowerUser')]
global?: boolean // Always available (like home pane)
mintClass?: NamedNode // RDF class this pane can create
mintNew?: (context, options) => Promise<NamedNode> // Create new instance
}// my-pane.js
export const myPane = {
name: 'my-custom-pane',
icon: 'https://example.com/icon.svg',
// label() determines if pane is relevant - MUST be synchronous
label: function (subject, context) {
const kb = context.session.store
const types = kb.findTypeURIs(subject)
// Return tooltip string if relevant, null if not
if (types['http://example.com/MyType']) {
return 'My Custom View'
}
return null
},
// render() creates the UI - can be async for lazy loading
render: function (subject, context) {
const div = document.createElement('div')
div.innerHTML = `<h2>Custom view for ${subject.uri}</h2>`
return div
}
}For better performance, use dynamic imports to load pane code on-demand:
// lazy/myPane.js - Lightweight wrapper (always loaded)
import * as UI from 'solid-ui-jss'
const ns = UI.ns
export const myPane = {
name: 'my-custom-pane',
icon: UI.icons.iconBase + 'custom-icon.svg',
// Sync label check - keep lightweight!
label: function (subject, context) {
const kb = context.session.store
const types = kb.findTypeURIs(subject)
if (types['http://example.com/MyType']) {
return 'My Custom View'
}
return null
},
// Async render - loads full pane code on demand
render: async function (subject, context) {
// Dynamic import creates a separate webpack chunk
const { render } = await import(
/* webpackChunkName: "my-pane" */
'../myPane/render.js'
)
return render(subject, context)
},
// Optional: create new instances
mintNew: async function (context, options) {
const { mintNew } = await import(
/* webpackChunkName: "my-pane" */
'../myPane/render.js'
)
return mintNew(context, options)
},
mintClass: ns.ex('MyType')
}// myPane/render.js - Full implementation (lazy loaded)
import * as UI from 'solid-ui-jss'
import { store } from 'solid-logic-jss'
export function render(subject, context) {
const div = document.createElement('div')
div.className = 'my-pane'
// Use solid-ui widgets
const header = UI.widgets.headerSection(subject, context)
div.appendChild(header)
// Query RDF data
const kb = context.session.store
const properties = kb.each(subject, null, null)
// Build UI...
const list = document.createElement('ul')
properties.forEach(prop => {
const li = document.createElement('li')
li.textContent = prop.value
list.appendChild(li)
})
div.appendChild(list)
return div
}
export async function mintNew(context, options) {
// Create a new instance of this type
const newUri = options.newBase + 'new-thing.ttl'
const subject = store.sym(newUri)
// Add RDF triples
const ins = []
ins.push($rdf.st(subject, ns.rdf('type'), ns.ex('MyType'), subject.doc()))
await store.updater.update([], ins)
return subject
}Add to registerPanes.js:
import { myPane } from './lazy/myPane.js'
export function registerPanes(register) {
// ... other panes
register(myPane)
}import { register } from 'solid-panes-jss'
import { myPane } from 'my-custom-pane'
register(myPane)import { register } from 'solid-panes-jss'
// Load pane definition from remote source
async function loadRemotePane(url) {
const module = await import(/* webpackIgnore: true */ url)
register(module.default || module.pane)
}
loadRemotePane('https://example.com/panes/calendar-pane.mjs')label: function (subject, context) {
const kb = context.session.store
const types = kb.findTypeURIs(subject)
// Match specific RDF types
if (types[ns.schema('Event').uri]) return 'Event'
if (types[ns.schema('Person').uri]) return 'Person'
return null
}label: function (subject, context) {
const kb = context.session.store
// Match if subject has specific predicates
if (kb.any(subject, ns.schema('startDate'))) {
return 'Calendar Event'
}
return null
}label: function (subject, context) {
const kb = context.session.store
const contentType = kb.anyValue(subject, ns.httph('content-type'))
if (contentType && contentType.includes('application/pdf')) {
return 'PDF Viewer'
}
return null
}label: function (subject, context) {
const kb = context.session.store
const types = kb.findTypeURIs(subject)
if (!types[ns.ldp('Container').uri]) return null
// Check if container has images
const contents = kb.each(subject, ns.ldp('contains'))
const hasImages = contents.some(item => {
const itemTypes = kb.findTypeURIs(item)
return itemTypes['http://purl.org/dc/terms/Image']
})
if (hasImages) return 'Photo Gallery'
return null
}solid-ui provides many reusable widgets:
import * as UI from 'solid-ui-jss'
function render(subject, context) {
const div = document.createElement('div')
// Forms
const form = UI.widgets.appendForm(div, subject, formDoc, formType, context)
// Buttons
UI.widgets.button(div, 'Click me', () => console.log('clicked'))
// ACL controls
UI.aclControl.ACLControlBox5(subject, context, div)
// Login status
UI.authn.loginStatusBox(div, context)
return div
}- Add to a local solid-panes-jss build
- Run
npm startin mashlib-jss - Navigate to a resource matching your
label()criteria - Your pane should appear in the pane selector
{
"name": "my-solid-pane",
"main": "dist/index.js",
"peerDependencies": {
"solid-ui-jss": "^3.0.0",
"solid-logic-jss": "^1.0.0",
"rdflib": "^2.0.0"
}
}Host a single .mjs file that exports your pane:
// https://example.com/panes/my-pane.mjs
export const pane = {
name: 'my-pane',
label: (s, c) => { /* ... */ },
render: (s, c) => { /* ... */ }
}- Keep label() lightweight - It runs for every subject, so avoid heavy computation
- Use lazy loading - Dynamic imports reduce initial bundle size
- Handle errors gracefully - Wrap render logic in try/catch
- Support async data - Use
kb.fetcher.load()for remote data - Use semantic HTML - Accessibility matters
- Follow existing patterns - Look at built-in panes for examples