Blocks & attributes
Describe editable block data
Attribute specs are how Uncial derives the editing UI you'd otherwise hand-build: each spec maps a component prop to a typed, validated control in the block attributes panel.
default— Required fallback value. Shorthand values like title: "" become defaults.input— Editor control hint: text, textarea, number, checkbox, json, richtext, select, image, or hidden. Left out, it is guessed from the default value.required— Marks an attribute as required during validation.validate— Custom predicate for accepting or rejecting an attribute value.parse / serialize— Coerce values at editor or persistence boundaries.options— Allowed values for select-style attributes.
Let authors add images
Give an attribute input: 'image' and the attributes panel shows an image field for it. The attribute stores a plain URL string.
// src/lib/blocks/figure.ts
import { defineSvelteBlock } from 'uncial';
import Figure from './Figure.svelte';
export const figure = defineSvelteBlock({
id: 'figure',
label: 'Figure',
attributes: {
src: { default: '', input: 'image' },
alt: ''
},
component: Figure
});Where images come from is up to you. Pass an imageSource to Editor with any of these three functions, and the field shows a button for each one you provide:
upload(file)— save the file somewhere and return the URL to store. Adds an Upload button.browse()— return the URLs of images that already exist. Adds a Choose existing button that opens a picker.thumbnail(src)— turn a stored URL into one the browser can show. Use it when the stored value is not directly displayable, such as an ID or a path without a base.
<script lang="ts">
import { Editor, type ImageSource } from 'uncial';
import { blocks, schema, attributesController } from './uncial';
const imageSource: ImageSource = {
async upload(file) {
const body = new FormData();
body.append('file', file);
const response = await fetch('/api/images', { method: 'POST', body });
return (await response.json()).url;
},
async browse() {
return (await fetch('/api/images')).json();
}
};
let document = $state({ type: 'doc', content: [{ type: 'paragraph' }] });
</script>
<Editor {blocks} {schema} {attributesController} {imageSource} bind:json={document} />Right after an upload, the editor shows the image from a local blob: copy, so it appears at once even if your server is slow to serve it. The document still stores the URL that upload returned.
Allow nested document flow
Nesting is where form-driven admins fall apart — and where in-place editing pays off most. Editors write nested content inside the rendered block, exactly where it will appear, not in a collapsed sub-form three scroll-screens away.
// src/lib/blocks/collapsible.ts
import { defineSvelteBlock } from 'uncial';
import Collapsible from './Collapsible.svelte';
export const collapsible = defineSvelteBlock({
id: 'collapsible',
label: 'Collapsible',
attributes: {
title: ''
},
component: Collapsible,
content: { kind: 'flow' }
});<!-- src/lib/blocks/Collapsible.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
title?: string;
children?: Snippet;
}
let { title = '', children }: Props = $props();
</script>
<details class="collapse collapse-arrow border border-base-300 bg-base-100">
<summary class="collapse-title font-semibold">{title}</summary>
<div class="collapse-content">
{#if children}
{@render children()}
{/if}
</div>
</details>Svelte ships first, core stays neutral
Svelte is the only bundled block runtime today. Non-Svelte block authoring is enabled through public runtime plugins that normalize native components and can provide editor node-view mounting. A registry may contain blocks from only one runtime in this release; mixed-runtime documents fail fast.
Svelte blocks keep the direct Svelte renderer path, so custom block rendering remains SSR-capable. Future React or Vue support should be implemented with full-document runtime renderers that can use each framework's SSR APIs, not by mixing client-only component islands into the Svelte renderer.
import { defineRuntimeBlock } from 'uncial/core';
import { reactRuntime } from 'uncial-react-runtime';
export function defineReactBlock(config) {
return defineRuntimeBlock(reactRuntime, config);
}