Skip to main content
Components are the foundation of Pala. You write components using Svelte, attach content fields to them, and they become blocks that editors can use to build pages. This guide covers everything you need to know about writing effective components.

Components vs Blocks

Understanding the distinction helps you build better: Components are pure Svelte code:
  • Reusable UI elements (Button, Card, Hero)
  • Accept props and handle logic
  • No content fields attached
  • Can be used anywhere in your Svelte code
Blocks are components with content fields:
  • Same Svelte component code
  • Plus editable content fields (text, images, links)
  • Used specifically in page types and pages
  • Editors can modify content, not code
When you create a block in Pala, you’re writing a Svelte component and then attaching fields to it. The component code stays the same; fields make it editable.

Component Structure

Every Pala component follows this structure:
<script>
  // 1. Import statements (if needed)
  // 2. Props (content fields)
  // 3. Reactive logic
</script>

<!-- 4. HTML markup -->

<style>
  /* 5. Scoped styles */
</style>

Writing Your First Component

Let’s build a simple Button component step by step:
1

Identify the fields you need

Think about what data your component needs. For a button block:
  • text: The button text
  • href: The link destination
  • variant: The button style (primary, secondary, outline)
Fields will be automatically available in your component code.
Plan your fields first, then reference them in your template. You can set default values in the field configuration.
2

Add the markup

Write the HTML structure:
<a href={href} class="button" class:primary={variant === "primary"} class:secondary={variant === "secondary"} class:outline={variant === "outline"}>
  {text}
</a>
3

Style the component

Add scoped styles:
<style>
  .button {
    display: inline-block;
    padding: 0.75rem 1.5rem;
    border-radius: 0.5rem;
    text-decoration: none;
    font-weight: 600;
    transition: all 0.2s;
  }
  
  .primary {
    background: #667eea;
    color: white;
  }
  
  .secondary {
    background: #764ba2;
    color: white;
  }
  
  .outline {
    border: 2px solid #667eea;
    color: #667eea;
    background: transparent;
  }
  
  .button:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }
</style>

Best Practices

1. Handle Empty Values

Components should work gracefully even with empty fields:
<!-- Good: Provides fallback values -->
<h1>{headline || "Welcome"}</h1>
<p>{subheadline || "Default description"}</p>

<!-- Also good: Conditional rendering -->
{#if headline}
  <h1>{headline}</h1>
{/if}

{#if subheadline}
  <p>{subheadline}</p>
{/if}

2. Use Conditional Rendering

Only show elements when content exists:
{#if image_url}
  <figure>
    <img src={image_url} alt={alt_text} />
    {#if caption}
      <figcaption>{caption}</figcaption>
    {/if}
  </figure>
{/if}

3. Handle Arrays Safely

When working with repeater fields, always check if the array has items:
{#if items && items.length > 0}
  <ul>
    {#each items as item}
      <li>{item.name}</li>
    {/each}
  </ul>
{:else}
  <p>No items to display</p>
{/if}

4. Use Scoped Styles

Pala components use scoped styles by default. Use :global() when you need to style child elements:
<style>
  .content :global(h2) {
    font-size: 2rem;
    margin-bottom: 1rem;
  }
  
  .content :global(p) {
    line-height: 1.8;
  }
</style>

5. Make Components Responsive

Always consider mobile devices:
<style>
  .grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 2rem;
  }

  @media (max-width: 768px) {
    .grid {
      grid-template-columns: 1fr;
      gap: 1rem;
    }
  }
</style>

6. Create Component Variations

Instead of creating many similar blocks, use fields to create variations of a single component: Layout variations:
<!-- Field: image_position (Select: "left", "right") -->
<div class="media-block" class:image-right={image_position === "right"}>
  <div class="image">
    {#if image?.url}
      <img src={image.url} alt={image.alt} />
    {/if}
  </div>
  <div class="content">
    <h2>{headline}</h2>
    <p>{description}</p>
  </div>
</div>

<style>
  .media-block {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2rem;
  }

  .media-block.image-right {
    direction: rtl;
  }

  .media-block.image-right > * {
    direction: ltr;
  }
</style>
Color/theme variations:
<!-- Field: variant (Select: "light", "dark", "primary") -->
<section class="hero variant-{variant}">
  <h1>{headline}</h1>
  <p>{description}</p>
</section>

<style>
  .hero {
    padding: 4rem 2rem;
    text-align: center;
  }

  .variant-light {
    background: #f5f5f5;
    color: #1a1a1a;
  }

  .variant-dark {
    background: #1a1a1a;
    color: #ffffff;
  }

  .variant-primary {
    background: var(--primary-color);
    color: #ffffff;
  }
</style>
Size variations:
<!-- Field: size (Select: "small", "medium", "large") -->
<button class="btn btn-{size}">
  {label}
</button>

<style>
  .btn {
    padding: 0.5rem 1rem;
    font-size: 1rem;
  }

  .btn-small {
    padding: 0.25rem 0.75rem;
    font-size: 0.875rem;
  }

  .btn-large {
    padding: 0.75rem 1.5rem;
    font-size: 1.25rem;
  }
</style>
One flexible block with variant options is easier to maintain than multiple similar blocks. Editors can adjust layouts without developers creating new components.

Working with Rich Content

Rich Text Fields

When using rich text fields, use {@html} to render HTML:
<div class="rich-text">
  {@html content}
</div>

<style>
  .rich-text :global(h2) {
    font-size: 2rem;
    margin-top: 2rem;
    margin-bottom: 1rem;
  }

  .rich-text :global(p) {
    margin-bottom: 1.5rem;
    line-height: 1.8;
  }

  .rich-text :global(ul), .rich-text :global(ol) {
    margin-bottom: 1.5rem;
    padding-left: 2rem;
  }
</style>
Only use {@html} with trusted content. Pala sanitizes rich text fields, but be cautious with user-generated content.

Markdown Content

If you’re using markdown fields, Pala processes them automatically. But you can also use a markdown processor for additional control:
<script>
  import { marked } from 'marked'; // If you add this dependency

  $: html_content = marked(markdown_content || "");
</script>

<div class="markdown-content">
  {@html html_content}
</div>

Advanced Patterns

Reactive Statements

Use reactive statements ($:) for computed values based on fields:
<script>
  $: displayed_items = (items || []).slice(0, max_items);
  $: has_more = (items || []).length > max_items;
</script>

{#each displayed_items as item}
  <div>{item.name}</div>
{/each}

{#if has_more}
  <p>And {items.length - max_items} more...</p>
{/if}

Event Handling

Handle user interactions:
<script>
  function handleClick() {
    // Analytics, tracking, etc.
    console.log('CTA clicked');
  }
</script>

<a href={cta_link} on:click={handleClick} class="cta">
  {cta_text}
</a>

Conditional Classes

Use dynamic classes for variants:
<div class="card" class:primary={variant === "primary"} class:large={size === "large"}>
  <!-- content -->
</div>

<style>
  .card.primary {
    background: #667eea;
    color: white;
  }

  .card.large {
    padding: 2rem;
  }
</style>

Component Composition

Build complex components from simpler ones:
<script>
  import Button from './Button.svelte';
</script>

<section class="hero">
  <h1>{headline}</h1>
  <p>{description}</p>
  <Button text={cta_text} href={cta_link} variant="primary" />
</section>
In Pala’s component editor, you write single-file components. For composition, you can create separate blocks and reference them, or write everything in one component file.

Performance Considerations

Lazy Loading Images

For image-heavy components:
<img src={image_url} alt={alt_text} loading="lazy" />

Optimize Re-renders

Use reactive statements efficiently:
<script>
  // Good: Only recalculates when items change
  $: sorted_items = [...(items || [])].sort((a, b) => a.name.localeCompare(b.name));
</script>

{#each sorted_items as item}
  <div>{item.name}</div>
{/each}

Testing Your Components

Use the Preview

Pala’s in-browser editor provides instant preview:
  1. Write your component code
  2. Add test content in fields
  3. See changes update in real-time
Test with empty fields, long text, special characters, and edge cases to ensure your component is robust.

Test Different Screen Sizes

Use browser dev tools in the preview to test:
  • Mobile (320px - 768px)
  • Tablet (768px - 1024px)
  • Desktop (1024px+)

Common Patterns

Card Component

<article class="card">
  {#if image_url}
    <img src={image_url} alt={title} />
  {/if}
  <div class="card-content">
    <h3>{title}</h3>
    {#if description}
      <p>{description}</p>
    {/if}
    {#if link}
      <a href={link} class="card-link">Read More →</a>
    {/if}
  </div>
</article>

<style>
  .card {
    border: 1px solid #e2e8f0;
    border-radius: 0.5rem;
    overflow: hidden;
  }

  .card-content {
    padding: 1.5rem;
  }
</style>

Grid Layout

<section class="grid" style="--columns: {columns}">
  {#each items as item}
    <div class="grid-item">{item.content}</div>
  {/each}
</section>

<style>
  .grid {
    display: grid;
    grid-template-columns: repeat(var(--columns), 1fr);
    gap: 2rem;
  }

  @media (max-width: 768px) {
    .grid {
      grid-template-columns: 1fr;
    }
  }
</style>

Accordion Component

<script>
  export let items = [];
  
  let open_index = null;
  
  function toggle(index) {
    open_index = open_index === index ? null : index;
  }
</script>

<div class="accordion">
  {#each items as item, index}
    <div class="accordion-item">
      <button class="accordion-header" on:click={() => toggle(index)}>
        {item.title}
        <span class="icon">{open_index === index ? '−' : '+'}</span>
      </button>
      {#if open_index === index}
        <div class="accordion-content">
          {item.content}
        </div>
      {/if}
    </div>
  {/each}
</div>

Next Steps

The best components are simple, focused, and reusable. Start with basic functionality, then add complexity only when needed.