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:
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.
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 >
Style the component
Add scoped styles: < style >
.button {
display : inline-block ;
padding : 0.75 rem 1.5 rem ;
border-radius : 0.5 rem ;
text-decoration : none ;
font-weight : 600 ;
transition : all 0.2 s ;
}
.primary {
background : #667eea ;
color : white ;
}
.secondary {
background : #764ba2 ;
color : white ;
}
.outline {
border : 2 px solid #667eea ;
color : #667eea ;
background : transparent ;
}
.button:hover {
transform : translateY ( -2 px );
box-shadow : 0 4 px 12 px 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 : 2 rem ;
margin-bottom : 1 rem ;
}
.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 , 1 fr );
gap : 2 rem ;
}
@media ( max-width : 768 px ) {
.grid {
grid-template-columns : 1 fr ;
gap : 1 rem ;
}
}
</ 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 : 1 fr 1 fr ;
gap : 2 rem ;
}
.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 : 4 rem 2 rem ;
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.5 rem 1 rem ;
font-size : 1 rem ;
}
.btn-small {
padding : 0.25 rem 0.75 rem ;
font-size : 0.875 rem ;
}
.btn-large {
padding : 0.75 rem 1.5 rem ;
font-size : 1.25 rem ;
}
</ 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 : 2 rem ;
margin-top : 2 rem ;
margin-bottom : 1 rem ;
}
.rich-text :global( p ) {
margin-bottom : 1.5 rem ;
line-height : 1.8 ;
}
.rich-text :global( ul ), .rich-text :global( ol ) {
margin-bottom : 1.5 rem ;
padding-left : 2 rem ;
}
</ 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 : 2 rem ;
}
</ 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.
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:
Write your component code
Add test content in fields
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 : 1 px solid #e2e8f0 ;
border-radius : 0.5 rem ;
overflow : hidden ;
}
.card-content {
padding : 1.5 rem ;
}
</ 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 ), 1 fr );
gap : 2 rem ;
}
@media ( max-width : 768 px ) {
.grid {
grid-template-columns : 1 fr ;
}
}
</ 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.