Skip to main content

Blocks Layer - UI Components

OverviewDirect link to Overview

For implementation details of the Blocks Layer in the standards-sdk, see the HCS-12 SDK Guide documentation.

The Blocks Layer implements a decentralized, composable UI component system based on WordPress's Gutenberg block format. Blocks are stored as immutable assets via HCS-1, with their definitions and templates stored separately. Assemblies reference blocks directly by their HCS-1 topic IDs.

Block ArchitectureDirect link to Block Architecture

Gutenberg FoundationDirect link to Gutenberg Foundation

HashLinks adopts the Gutenberg block format for its proven flexibility and extensive ecosystem. Key benefits include:

  1. Composability: Blocks can contain other blocks, enabling complex layouts
  2. Portability: Standardized format works across different implementations
  3. Extensibility: Rich attribute system for customization
  4. Accessibility: Built-in support for screen readers and keyboard navigation

Block Storage StructureDirect link to Block Storage Structure

Blocks consist of two parts stored via HCS-1:

  1. Block Template (HTML): The rendering template with placeholders
  2. Block Definition (JSON): The Gutenberg block metadata including template reference

The template is stored first, then referenced in the block definition:

{
"apiVersion": 3,
"name": "hashlink/token-transfer",
"title": "Token Transfer",
"category": "hashlink/actions",
"icon": "money",
"description": "Transfer tokens between accounts",
"keywords": ["token", "transfer", "payment"],
"template_t_id": "0.0.123456",
"attributes": {
"tokenId": {
"type": "string",
"default": ""
},
"amount": {
"type": "number",
"default": 1
}
},
"supports": {
"align": ["wide", "full"],
"anchor": true
}
}

This approach keeps the template separate (avoiding JSON escaping issues) while making blocks self-contained.

Gutenberg CompatibilityDirect link to Gutenberg Compatibility

HashLinks blocks are designed to be fully compatible with the WordPress Gutenberg editor, enabling:

  1. Drag-and-Drop Editing: Users can visually arrange blocks using any Gutenberg-compatible editor
  2. Visual Block Selection: Browse and insert blocks from a visual library
  3. Live Preview: See changes in real-time as blocks are configured
  4. Nested Composition: Create complex layouts with container blocks
  5. Responsive Design: Blocks automatically adapt to different screen sizes

Using Gutenberg EditorsDirect link to Using Gutenberg Editors

HashLinks blocks can be created and edited using any Gutenberg-compatible editor:

  1. Open Editor: Use WordPress, Gutenberg standalone, or any compatible block editor
  2. Insert Blocks: Browse the HashLink category in the block inserter
  3. Configure: Use the block settings panel to configure attributes
  4. Preview: See real-time preview of your HashLink experience
  5. Export: Save the block structure as JSON for registration

Block Creation ProcessDirect link to Block Creation Process

Blocks are created by storing their template first, then their definition:

Step 1: Store Block TemplateDirect link to Step 1: Store Block Template

Store the HTML template via HCS-1:

<!-- token-transfer-template.html -->
<div class="hashlink-block token-transfer" data-block-id="{{blockId}}">
<h3>{{attributes.title}}</h3>

<form data-action="{{actionTopicId}}" data-params='{"operation": "transfer"}'>
<label>
Token ID:
<input type="text" name="tokenId" value="{{attributes.tokenId}}" />
</label>

<label>
Recipient:
<input type="text" name="recipient" value="{{attributes.recipient}}" />
</label>

<label>
Amount:
<input type="number" name="amount" value="{{attributes.amount}}" />
</label>

<button type="submit">Transfer</button>
</form>

{{#if actionResults.transfer}}
<div class="result success">
Transfer complete! TX: {{actionResults.transfer.transactionId}}
</div>
{{/if}}
</div>

Store this template via HCS-1. The returned topic ID (e.g., "0.0.123456") will be referenced in the block definition.

Step 2: Store Block DefinitionDirect link to Step 2: Store Block Definition

Store the block definition with template reference:

Block definition JSON:

{
"apiVersion": 3,
"name": "hashlink/token-transfer",
"title": "Token Transfer Form",
"category": "hashlink/actions",
"description": "Transfer tokens with a customizable form",
"keywords": ["token", "transfer", "hedera"],
"template_t_id": "0.0.123456",
"attributes": {
"tokenId": {
"type": "string",
"default": ""
},
"amount": {
"type": "number",
"default": 1
},
"recipient": {
"type": "string",
"default": ""
}
},
"supports": {
"align": ["wide", "full"],
"anchor": true
}
}

Store this JSON via HCS-1. The returned topic ID (e.g., "0.0.123457") is used to reference the block.

Step 3: Use in AssemblyDirect link to Step 3: Use in Assembly

Reference the block in an assembly by its definition topic ID:

{
"p": "hcs-12",
"op": "add-block",
"block_t_id": "0.0.123457",
"actions": {
"transfer": "0.0.789012"
},
"attributes": {
"tokenId": "0.0.456789",
"amount": 100
}
}

Template SystemDirect link to Template System

HTML TemplatesDirect link to HTML Templates

Block templates use standard HTML with Handlebars-style placeholders and include resources via HCS-3:

<link
rel="stylesheet"
data-src="hcs://1/0.0.111111"
data-script-id="token-transfer-styles"
/>

<div
class="hashlink-block {{attributes.className}}"
data-block-id="{{blockId}}"
>
<h3>{{attributes.title}}</h3>

{{#if attributes.showDescription}}
<p>{{attributes.description}}</p>
{{/if}}

<form data-action="{{actions.0}}">
<label>
Token ID:
<input type="text" name="tokenId" value="{{attributes.tokenId}}" />
</label>

<label>
Amount:
<input type="number" name="amount" value="{{attributes.defaultAmount}}" />
</label>

{{#if attributes.showAdvanced}}
<div class="advanced-options">
<!-- Additional fields -->
</div>
{{/if}}

<button type="submit">Transfer</button>
</form>

{{#if action.result}}
<div class="result">{{ action.result.message }}</div>
{{/if}}
</div>

<!-- Block-specific JavaScript -->
<script
data-src="hcs://1/0.0.222222"
data-script-id="token-transfer-logic"
></script>

Resource Loading with HCS-3Direct link to Resource Loading with HCS-3

Resources are loaded using the HCS-3 protocol:

<link
rel="stylesheet"
data-src="hcs://1/0.0.123456"
data-script-id="block-styles"
/>

<script
data-src="hcs://3/0.0.789012"
data-dependencies="['jquery', 'react']"
></script>

<img
data-src="hcs://1/{{ attributes.imageTopicId }}"
alt="{{ attributes.imageAlt }}"
loading="lazy"
/>

Action BindingDirect link to Action Binding

Blocks bind to actions using topic IDs directly. This ensures deterministic behavior and clear dependencies.

Declaring Actions in AssemblyDirect link to Declaring Actions in Assembly

When adding a block to an assembly, provide a map of action names to topic IDs:

{
"p": "hcs-12",
"op": "add-block",
"block_t_id": "0.0.123456",
"actions": {
"primary": "0.0.789012"
},
"attributes": {
"title": "Transfer HBAR"
}
}

Using Actions in TemplatesDirect link to Using Actions in Templates

Templates receive actions as a map of names to topic IDs:

<button data-action="{{actions.primary}}">Submit Payment</button>

<form data-action="{{actions.primary}}" data-params='{"operation": "transfer"}'>
<input type="text" name="recipient" />
<input type="number" name="amount" />
<button type="submit">Transfer</button>
</form>

<div data-action-click="{{actions.primary}}" data-params='{"amount": 100, "recipient": "0.0.123456"}'>
Quick Transfer 100 HBAR
</div>

For blocks with multiple actions:

{
"p": "hcs-12",
"op": "add-block",
"block_t_id": "0.0.123456",
"actions": {
"transfer": "0.0.789012",
"validate": "0.0.789013",
"getBalance": "0.0.789014"
}
}

In the template:

<form data-action="{{actions.transfer}}" data-validate="{{actions.validate}}">
<button type="button" data-action-click="{{actions.getBalance}}">Check Balance</button>
<input type="text" name="recipient" />
<button type="submit">Transfer</button>
</form>

Action Execution FlowDirect link to Action Execution Flow

  1. User Interaction: User clicks button or submits form
  2. Data Collection: Form data or data-params are collected
  3. Action Loading: The WASM module is loaded from the action topic ID
  4. WASM Execution: The action's POST method is called with:
    • action: The method name from the action definition
    • params: Collected form data or specified parameters
    • network: Current Hedera network
    • hashLinkMemo: Assembly identifier
  5. Result Handling: The WASM response updates the block state

Event HandlersDirect link to Event Handlers

Blocks can specify different events for actions:

<button data-action-click="{{actions.primary}}">Click Me</button>

<select data-action-change="{{actions.validate}}" name="token">
<option value="HBAR">HBAR</option>
<option value="USDC">USDC</option>
</select>

<div data-action-mouseover="{{actions.preview}}"
data-action-params='{"hover": true}'>
Hover for preview
</div>

Passing ParametersDirect link to Passing Parameters

Parameters can be passed to actions in several ways:

<form data-action="{{actions.transfer}}">
<input name="recipient" type="text" />
<input name="amount" type="number" />
<button type="submit">Send</button>
</form>

<button data-action="{{actions.transfer}}"
data-params='{"amount": 50, "token": "HBAR"}'>
Send 50 HBAR
</button>

<button data-action="{{actions.transfer}}"
data-params='{"tokenId": "{{attributes.tokenId}}"}'>
Transfer {{attributes.tokenName}}
</button>

Action ResultsDirect link to Action Results

Action results are available for template updates:

{{#if actionResults.transfer}}
<div class="success">
Transfer complete!
TX: {{actionResults.transfer.transactionId}}
</div>
{{/if}}

{{#if actionErrors.transfer}}
<div class="error">
Transfer failed: {{actionErrors.transfer.message}}
</div>
{{/if}}

Nested Blocks (Recursive Composition)Direct link to Nested Blocks (Recursive Composition)

HashLinks supports nested blocks through template-based composition. Container blocks can include other blocks directly in their templates, enabling flexible layouts without requiring explicit child references.

Template-Based CompositionDirect link to Template-Based Composition

Blocks compose other blocks using HashLink references directly in templates:

<div class="dashboard-container" data-block-id="{{blockId}}">
<h2>{{attributes.title}}</h2>

<!-- Include other blocks using hcs:// protocol -->
<div class="stats-section">
<div data-hashlink="hcs://12/{{attributes.statsBlockId}}"
data-attributes='{"theme": "{{attributes.theme}}"}'>
<!-- Stats block renders here -->
</div>
</div>

<!-- Include with HCS-2 non-indexed reference -->
<div class="actions-section">
<div data-hashlink="hcs://2/{{attributes.actionsRegistry}}/transfer-form"
data-attributes='{"tokenId": "{{attributes.defaultToken}}"}'>
<!-- Transfer form renders here -->
</div>
</div>

<!-- Static block reference -->
<div data-hashlink="hcs://12/0.0.123456">
<!-- Fixed block always included -->
</div>
</div>

Dynamic Block ListsDirect link to Dynamic Block Lists

For dynamic lists of blocks, use template helpers:

<div class="portfolio-grid">
{{#each attributes.portfolioItems}}
<div class="portfolio-item"
data-hashlink="hcs://12/{{this.blockId}}"
data-attributes='{"tokenId": "{{this.tokenId}}", "amount": "{{this.amount}}"}'>
<!-- Each portfolio item renders here -->
</div>
{{/each}}
</div>

Child Block ContextDirect link to Child Block Context

Child blocks inherit context from their parent:

<!-- Parent block template -->
<div class="parent-block" data-theme="{{attributes.theme}}">
<h2>{{attributes.title}}</h2>

<!-- Children inherit parent context -->
<div class="children-wrapper">
{{{children}}}
</div>
</div>

<!-- Child block can access parent context -->
<div class="child-block">
<h3>{{attributes.name}}</h3>
<!-- Access parent theme via context -->
<p class="themed-{{parentContext.attributes.theme}}">
{{attributes.content}}
</p>
</div>

Recursive Block ExampleDirect link to Recursive Block Example

A dashboard composing nested sections through its template:

<!-- Dashboard Block Template -->
<div class="defi-dashboard">
<h1>{{attributes.title}}</h1>

<!-- Stats Section (which itself contains nested blocks) -->
<section class="stats">
<div data-hashlink="hcs://12/{{attributes.statsContainerId}}"
data-attributes='{
"title": "Portfolio Stats",
"cardRegistry": "{{attributes.statsCardRegistry}}",
"cardIds": {{json attributes.statCardIds}}
}'>
</div>
</section>

<!-- Actions Section -->
<section class="actions">
<div data-hashlink="hcs://12/{{attributes.actionsBlockId}}"
data-attributes='{
"defaultToken": "{{attributes.defaultToken}}"
}'
data-actions='{
"transfer": "{{actions.transfer}}",
"swap": "{{actions.swap}}"
}'>
</div>
</section>
</div>

The Stats Container block template can then include its own nested blocks:

<!-- Stats Container Template -->
<div class="stats-container">
<h2>{{attributes.title}}</h2>
<div class="stats-grid">
{{#each attributes.cardIds}}
<div data-hashlink="hcs://2/{{../attributes.cardRegistry}}/{{this}}"
class="stat-card">
</div>
{{/each}}
</div>
</div>

State Management for Nested BlocksDirect link to State Management for Nested Blocks

Each block maintains its own state, with child states accessible to parents:

// Parent block state
{
"attributes": {
"title": "Dashboard",
"theme": "dark"
},
"actionResults": {},
"childStates": {
"0.0.100001": {
"attributes": { "value": 1000 },
"actionResults": {}
},
"0.0.100002": {
"attributes": { "status": "ready" },
"actionResults": {}
}
}
}

Event BubblingDirect link to Event Bubbling

Events from child blocks can bubble up to parents:

<!-- Parent template -->
<div class="parent" data-block-id="{{blockId}}"
data-handle-child-events="true">
<h2>{{attributes.title}}</h2>
{{{children}}}

{{#if childEvents.updated}}
<p>Child {{childEvents.updated.blockId}} was updated</p>
{{/if}}
</div>

Block PatternsDirect link to Block Patterns

Pre-configured block arrangements can be registered as patterns:

{
"p": "hcs-12",
"op": "pattern",
"name": "defi-dashboard",
"title": "DeFi Dashboard",
"description": "Complete DeFi dashboard layout",
"categories": ["hashlink/patterns"],
"content": {
"name": "hashlink/container",
"attributes": {
"layout": "grid",
"columns": 2
},
"innerBlocks": [
{
"name": "hashlink/balance-display",
"attributes": {
"tokenId": "0.0.123456"
}
},
{
"name": "hashlink/token-transfer-form",
"attributes": {
"tokenId": "0.0.123456"
}
}
]
}
}

Security ConsiderationsDirect link to Security Considerations

Template SanitizationDirect link to Template Sanitization

All HTML templates MUST be sanitized:

  • Remove script tags (use HCS-3 for scripts)
  • Sanitize event handlers
  • Validate URLs and links
  • Escape user-provided content

Action ValidationDirect link to Action Validation

Blocks MUST validate action bindings:

  • Verify action exists in registry
  • Check if block is allowed to use the action
  • Validate that required actions are available

Resource VerificationDirect link to Resource Verification

All resources loaded via HCS-3 MUST be verified:

  • Check resource hashes
  • Validate MIME types
  • Enforce size limits

Step 1: Store Block DefinitionDirect link to Step 1: Store Block Definition

Store the Gutenberg block definition via HCS-1:

First, store the template HTML via HCS-1. This returns topic ID: 0.0.456788

Step 2: Store Block DefinitionDirect link to Step 2: Store Block Definition

Block definition JSON:

{
"apiVersion": 3,
"name": "hashlink/nft-gallery",
"title": "NFT Gallery",
"category": "hashlink/media",
"description": "Display NFTs in a responsive gallery",
"template_t_id": "0.0.456788",
"attributes": {
"collectionId": {
"type": "string",
"default": ""
},
"columns": {
"type": "number",
"default": 3
},
"showPrice": {
"type": "boolean",
"default": true
}
},
"supports": {
"align": ["wide", "full"],
"spacing": {
"margin": true,
"padding": true
}
}
}

Store this JSON via HCS-1. This returns topic ID: 0.0.456789

Template HTML:

<div class="nft-gallery" data-columns="{{attributes.columns}}">
{{#each nfts}}
<div class="nft-item">
<img src="{{this.image}}" alt="{{this.name}}" />
<h4>{{this.name}}</h4>

{{#if ../attributes.showPrice}}
<p class="price">{{this.price}} ℏ</p>
{{/if}}

<div class="actions">
<button data-action-click="{{actions.viewDetails}}" data-nft-id="{{this.id}}">
View
</button>
<button data-action-click="{{actions.purchase}}" data-nft-id="{{this.id}}">
Buy Now
</button>
</div>
</div>
{{/each}}
</div>

Step 3: Use in AssemblyDirect link to Step 3: Use in Assembly

Add the block to an assembly with action bindings:

{
"p": "hcs-12",
"op": "add-block",
"block_t_id": "0.0.456789",
"actions": {
"viewDetails": "0.0.789012",
"purchase": "0.0.789013"
},
"attributes": {
"collectionId": "0.0.123456",
"columns": 4,
"showPrice": true
}
}

Best PracticesDirect link to Best Practices

  1. Follow Gutenberg Standards: Use standard Gutenberg block structure
  2. Keep Templates Simple: Complex logic belongs in actions, not templates
  3. Use Semantic HTML: Ensure accessibility
  4. Responsive Design: Test on all screen sizes
  5. Version Carefully: Blocks are immutable once registered
  6. Document Attributes: Clear descriptions for all block settings
  7. Action Binding:
    • Use descriptive action aliases that match their purpose
    • Validate parameters before sending to actions
    • Handle both success and error states from actions
    • Consider loading states for async action execution
    • Use data-params for static values, forms for user input