About PineUI
PineUI is a Server-Driven UI framework developed by David Ruiz (wupsbr) in Rio de Janeiro and SΓ£o Paulo, Brazil.
Try it out: See PineUI in action with our interactive demos
Why "PineUI"? π
In Brazilian Portuguese, we have an expression: "descascar esse abacaxi" (literally "peeling this pineapple"), which means solving a tough problem. Any developer who has worked on real-world projects knows the challenges:
- Building new systems from scratch while maintaining legacy ones
- Keeping multiple platforms (Web, iOS, Android) in sync
- Iterating quickly without breaking production
- Modernizing old codebases without complete rewrites
- Enabling non-technical teams to experiment and innovate
These are the "pineapples" (tough problems) that PineUI helps you solve. The name is a playful reminder that software development is challenging β but with the right tools, you can peel through those challenges smoothly.
The Vision
PineUI was born from the need to simplify the development of dynamic, cross-platform projects in a world increasingly driven by Artificial Intelligence. In an era where Large Language Models (LLMs) are becoming capable of generating and modifying user interfaces, we needed a solution that enables:
- Full Declarative: 100% JSON-defined interfaces with zero imperative code
- Cross-Platform: Single schema works on Web (React) and Mobile (Flutter)
- AI-Friendly: LLMs can easily generate and modify PineUI schemas without deep framework knowledge
- Server-Driven: Update UIs without redeployment, perfect for A/B testing and dynamic content
- Productivity: Faster development with ready-to-use, reusable components
Why PineUI?
In a world where applications need to be increasingly dynamic and personalized, PineUI offers an elegant solution: separate presentation logic (UI) from business logic (backend), allowing your server to completely control how the interface is rendered.
This is especially powerful in AI-native applications, where different users can have completely personalized experiences based on context, preferences, and even generated in real-time by language models.
Impact & Innovation Acceleration
PineUI is more than a framework β it's a paradigm shift in how we build software. In an era where companies need to innovate faster than ever, PineUI enables:
π Accelerating New Solutions
Build entirely new products and experiences 5-10x faster by letting LLMs generate UI schemas. Instead of spending weeks designing and implementing UI components across multiple platforms, describe what you want in natural language, and let AI generate the PineUI schema. The same schema instantly works on Web, Mobile, and any future platform.
π Modernizing Legacy Systems
One of PineUI's most powerful use cases is accelerating innovation within legacy systems. Instead of rewriting your entire backend, expose your legacy APIs and let PineUI provide a modern, dynamic frontend. This hybrid approach allows you to:
- Keep your battle-tested business logic intact
- Deliver modern, responsive UIs without full rewrites
- Iterate on user experience independently from backend changes
- Gradually modernize systems without big-bang migrations
- Test new features with A/B testing before committing to backend changes
π€ LLM-Powered Development
PineUI is designed to be the perfect bridge between human intent and machine execution. LLMs can:
- Generate complete UI schemas from natural language descriptions
- Modify existing schemas based on user feedback
- Create personalized experiences for each user based on context
- A/B test variations without developer intervention
- Adapt interfaces in real-time based on user behavior
πΌ Real Business Value
Companies using Server-Driven UI approaches have reported:
- 80% reduction in time-to-market for new features
- 90% decrease in mobile app deployment cycles
- 5-10x increase in A/B testing velocity
- Zero downtime for UI updates and experiments
- Unified codebase across all platforms
π Bridging Old and New
The real innovation isn't just building new things β it's making existing systems better, faster. PineUI enables companies to:
- Add modern mobile apps to legacy web systems without backend changes
- Create dynamic dashboards on top of old APIs
- Modernize customer-facing experiences while preserving core business logic
- Experiment with new UX patterns without engineering bottlenecks
- Enable product teams to iterate independently from engineering cycles
π‘ Real-World Example
Imagine a 20-year-old banking system with stable, audited business logic but outdated UI. Instead of a risky multi-year rewrite, you can:
- Keep the core banking APIs untouched
- Build a PineUI schema that consumes those APIs
- Deploy a modern React web app + Flutter mobile app in weeks
- Let LLMs generate personalized dashboards for different user segments
- A/B test new features by just changing JSON on the server
- Roll out updates instantly without app store deployments
This is the power of Server-Driven UI combined with AI: innovation without disruption.
About the Creator
David Ruiz (wupsbr) is a seasoned technology leader with over 20 years of experience building and scaling high-performance teams across engineering, product, data, design, infrastructure, and security.
Current Role: Chief Product and Technology Officer (CPTO) at Ingresse, one of Brazil's largest ticketing and event platforms, leading Product, Engineering, Data, Security, Infrastructure, and Design teams. Currently building Ingresse's new global platform from scratch using generative AI, while managing operational stability, cost efficiency, cybersecurity maturity, and international expansion.
Previous Experience:
- iFood: Director of Engineering, created and scaled iFood BenefΓcios and iFood Pago, ensuring financial stability for operations exceeding R$70 billion in GMV annually
- ParanΓ‘ Banco: CTO, led large-scale digital transformation and platform modernization
- Elo (CartΓ£o Elo): Superintendent, conducted enterprise-wide digital transformations and risk mitigation strategies
Entrepreneurship: Co-founder of ONOVOLAB, one of Brazil's leading innovation hubs. Transformed a 21,000mΒ² former textile factory in SΓ£o Carlos into a vibrant technology and entrepreneurship hub. ONOVOLAB has hosted visitors from 23 countries, was featured in Forbes, appeared on Nasdaq's billboard, and attracted companies like Santander/F1RST, iFood, and LuizaLabs, while housing dozens of growing startups.
π Built for Real Innovation
"I believe the most powerful technology is that which brings people together, accelerates ideas, and promotes meaningful transformation. I've been using generative AI (OpenAI, Claude, Bolt.new, V0.dev, lovable.dev) to scale products, decisions, and teams β accelerating software development by up to 5x and unlocking new levels of creativity, efficiency, and strategic impact."
β David Ruiz
PineUI solves real problems faced by companies that innovate constantly: rapid iteration, multi-platform deployment, AI-generated interfaces, and the ability to update user experiences without code deployments. It's built by someone who has scaled billion-dollar platforms and understands the challenges of modern software development at enterprise scale.
Key Features
- Reactive state management with automatic UI updates
- Intent-based action system for clean separation of concerns
- Built-in HTTP client for API integration
- Collection rendering with virtualization support
- Modal and overlay system
- Custom component library support
- Data binding with {{}} syntax
- Material Design 3 components out of the box
Roadmap
PineUI is constantly evolving, with focus on:
- Expanding the component library
- Performance improvements and optimizations
- Native LLM integration for automatic UI generation
- Visual tools for schema building
- Support for more platforms (iOS native, Android native, Desktop)
- Developer experience improvements
π€ Contribute
PineUI is an open-source project licensed under Apache 2.0 + Commons Clause. Contributions are welcome! Visit our GitHub repository to report bugs, suggest features, or contribute code.
π§ Contact
For questions, suggestions, or commercial licensing:
Email: wupsbr@gmail.com
Creator: David Ruiz (wupsbr)
LinkedIn: linkedin.com/in/davidruiz
Company: Luma Ventures Ltda (CNPJ: 21.951.820/0001-39)
Installation
React
{
"dependencies": {
"@pineui/react": "^0.1.0"
}
}
CDN (Standalone)
<!-- CSS --> <link rel="stylesheet" href="https://unpkg.com/@pineui/react@latest/dist/style.css"> <!-- JavaScript (includes React) --> <script src="https://unpkg.com/@pineui/react@latest/dist/pineui.standalone.js"></script>
Quick Start
Basic Example
{
"schemaVersion": "1.0.0",
"screen": {
"type": "layout.column",
"padding": 16,
"spacing": 16,
"children": [
{
"type": "text",
"content": "Hello PineUI!",
"style": "titleLarge"
},
{
"type": "button.filled",
"label": "Click Me",
"onPress": {
"type": "action.snackbar.show",
"message": "Button clicked!"
}
}
]
}
}
With State Management
{
"schemaVersion": "1.0.0",
"state": {
"counter": 0
},
"intents": {
"increment": {
"handler": {
"type": "action.state.patch",
"path": "counter",
"value": "{{state.counter + 1}}"
}
}
},
"screen": {
"type": "layout.column",
"padding": 16,
"spacing": 16,
"children": [
{
"type": "text",
"content": "Count: {{state.counter}}",
"style": "titleLarge"
},
{
"type": "button.filled",
"label": "Increment",
"onPress": {
"intent": "increment"
}
}
]
}
}
π€ Using with LLMs
PineUI is designed to be AI-friendly. LLMs like ChatGPT, Claude, and others can generate complete UIs using PineUI's declarative JSON schema - no specific training required!
Why LLMs Love PineUI
- Declarative - Describe what you want, not how to build it
- JSON-based - Native format for LLMs to generate
- Material Design - Built-in components with consistent naming
- Self-contained - Complete apps in a single schema
Complete LLM Context Guide
Copy and paste this complete guide when using ChatGPT, Claude, or any LLM to build PineUI applications:
π View complete PROMPT.md guide
Click to load the guide...
Quick Example Prompt
Copy and paste this prompt into ChatGPT, Claude, or any LLM:
Create a complete HTML page with a PineUI application that displays a gallery of online courses with category filtering. Requirements: 1. Use PineUI from CDN: - JS: https://unpkg.com/@pineui/react@latest/dist/pineui.standalone.js - CSS: https://unpkg.com/@pineui/react@latest/dist/style.css 2. The app should have: - Header with app bar showing "Course Gallery" title - Row of filter chips for categories: All, Design, Development, Business, Marketing - Grid layout (3 columns on desktop) showing course cards - Each card should show: thumbnail image, category badge, title, instructor name, rating, and price 3. State management: - selectedCategory state (default "All") - When a chip is clicked, update selectedCategory - Use conditional rendering to filter courses by category 4. Sample courses data (at least 9 courses across different categories): - Include realistic course titles, instructors, ratings, and prices - Use placeholder images from https://picsum.photos/400/225 5. Styling: - Use Material Design 3 colors - Responsive grid (3 columns on desktop, 2 on tablet, 1 on mobile) - Selected chip should be filled style, others outlined Use PineUI.render() to mount the application to a div with id="app".
What the LLM Will Generate
The LLM will create a complete, working application with:
- Proper HTML structure with PineUI CDN links
- Complete JSON schema with state management
- Interactive category filtering
- Responsive grid layout
- Material Design 3 styling
Tips for Better Results
- Be specific - Describe exact components and layouts you want
- Reference CDN - Always include the unpkg CDN links for PineUI
- Mention state - If you need interactivity, specify state variables and actions
- Include examples - Reference component types like "button.filled", "layout.column", etc.
- Ask for variations - Request the LLM to iterate or add features
Common Use Cases
- π± Mobile-first landing pages
- π¨ Product galleries and catalogs
- π Data dashboards with charts
- π Forms and surveys
- π E-commerce product listings
- π° News feeds and blogs
π‘ Pro Tip: After the LLM generates the initial UI, you can ask it to add features, change styling, or refactor components. PineUI's declarative nature makes iterations easy!
Schema Structure
A PineUI schema is a JSON object that defines your entire application structure.
Root Properties
| Property | Type | Description |
|---|---|---|
| schemaVersion | string | Schema version (currently "1.0.0") |
| imports | object | External component imports |
| state | object | Initial application state |
| intents | object | Intent definitions (actions handlers) |
| screen | ComponentNode | Root UI component |
| components | object | Custom component definitions |
| overlays | object | Modal and overlay definitions |
Components
PineUI provides a comprehensive set of Material Design 3 components that can be composed to build complex user interfaces. All components support data binding and can be nested to create rich layouts.
Component Categories
- Layout: Structure your UI with rows, columns, and spacing
- Display: Show text, images, avatars, and cards
- Input: Text fields and form controls
- Action: Buttons, FABs, and interactive elements
- Data: Collections and conditional rendering
π‘ Component Composition
All PineUI components can be composed together. Use layout components (row, column) to arrange other components, and nest them as deeply as needed.
State Management
PineUI includes a reactive state management system. When state changes, the UI automatically
updates to reflect the new values. State can be accessed using {{state.propertyName}} syntax.
Initial State
Define your initial state in the schema root:
{
"state": {
"username": "",
"counter": 0,
"items": [],
"settings": {
"darkMode": false,
"notifications": true
}
}
}
Updating State
Use the action.state.patch action to update state:
{
"type": "action.state.patch",
"path": "counter",
"value": "{{state.counter + 1}}"
}
Nested State
Access and update nested state using dot notation:
{
"type": "action.state.patch",
"path": "settings.darkMode",
"value": true
}
π‘ Reactivity
State updates are automatically reactive. Any component that references a state value will re-render when that value changes.
Intents
Intents are named action handlers that encapsulate business logic. They provide a clean separation between UI components and actions, making your schemas more maintainable.
Defining Intents
{
"intents": {
"loadUsers": {
"handler": {
"type": "action.http",
"method": "GET",
"url": "https://api.example.com/users",
"onSuccess": {
"type": "action.state.patch",
"path": "users",
"value": "{{response}}"
}
}
},
"deleteUser": {
"handler": [
{
"type": "action.http",
"method": "DELETE",
"url": "https://api.example.com/users/{{props.userId}}"
},
{
"intent": "loadUsers"
}
]
}
}
}
Calling Intents
Trigger intents from component actions:
{
"type": "button.filled",
"label": "Load Data",
"onPress": {
"intent": "loadUsers"
}
}
Intent Props
Pass parameters to intents:
{
"onPress": {
"intent": "deleteUser",
"props": {
"userId": "{{item.id}}"
}
}
}
π‘ Action Chains
Intents can execute multiple actions in sequence by using an array. This is perfect for workflows like: make API call β update state β show success message.
Actions
Actions are the building blocks of interactivity in PineUI. They handle HTTP requests, state updates, navigation, and more. Actions can be triggered from component events or executed within intents.
Action Types
action.http- Make HTTP requests to APIsaction.state.patch- Update application stateaction.overlay.open/close- Control modals and overlaysaction.snackbar.show- Display toast notificationsaction.delay- Add timing delaysaction.collection.refresh- Refresh collection data
Action Chaining
Execute multiple actions in sequence:
{
"onPress": [
{
"type": "action.state.patch",
"path": "loading",
"value": true
},
{
"type": "action.http",
"method": "POST",
"url": "/api/save"
},
{
"type": "action.state.patch",
"path": "loading",
"value": false
},
{
"type": "action.snackbar.show",
"message": "Saved successfully!"
}
]
}
Text Component
Display text with Material Design 3 typography styles.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "text" | Component type identifier |
| content required | string | Text content to display. Supports {{bindings}} |
| style | string | Typography style: "titleLarge", "titleMedium", "titleSmall", "bodyLarge", "bodyMedium", "bodySmall", "headlineSmall" |
| color | string | Text color (hex, Material color token, or CSS color) |
| fontWeight | string | "normal", "bold", or numeric (400, 600, 700, etc.) |
| fontSize | number | Font size in pixels |
| lineHeight | number | Line height multiplier |
Example
{
"type": "text",
"content": "Hello World",
"style": "titleLarge",
"color": "#6750A4",
"fontWeight": "bold"
}
π‘ Tip
Use Material Design color tokens like onSurface, primary, onSurfaceVariant for consistent theming across platforms.
Button Component
Buttons trigger actions when pressed. PineUI provides multiple button types following Material Design 3 guidelines: filled, outlined, text, icon, and floating action buttons (FAB).
Button Types
button.filled- High emphasis, primary actionsbutton.outlined- Medium emphasis, secondary actionsbutton.text- Low emphasis, tertiary actionsbutton.icon- Icon-only button for toolbarsbutton.fab- Floating action button for primary screen actions
Properties
| Property | Type | Description |
|---|---|---|
| type required | string | Button type: "button.filled", "button.outlined", "button.text", "button.icon", "button.fab" |
| label | string | Button text label. Supports {{bindings}}. Required for filled/outlined/text buttons |
| icon | string | Material icon name (e.g., "add", "delete", "edit"). Required for icon and fab buttons |
| onPress | Action | Action[] | Action(s) to execute when button is pressed |
| disabled | boolean | Disable button interaction. Can use {{bindings}} |
| fullWidth | boolean | Make button expand to full width of container |
| color | string | Custom button color (hex or Material color token) |
| size | string | For FAB: "small", "medium", "large". Default: "medium" |
Examples
Filled Button
{
"type": "button.filled",
"label": "Submit",
"onPress": {
"intent": "submitForm"
}
}
Outlined Button
{
"type": "button.outlined",
"label": "Cancel",
"onPress": {
"type": "action.overlay.close"
}
}
Text Button
{
"type": "button.text",
"label": "Learn More",
"onPress": {
"type": "action.http",
"method": "GET",
"url": "/api/details"
}
}
Icon Button
{
"type": "button.icon",
"icon": "favorite",
"onPress": {
"type": "action.state.patch",
"path": "isFavorite",
"value": true
}
}
Floating Action Button
{
"type": "button.fab",
"icon": "add",
"size": "large",
"onPress": {
"type": "action.overlay.open",
"overlayId": "createItemModal"
}
}
Button with Icon and Label
{
"type": "button.filled",
"label": "Download",
"icon": "download",
"onPress": {
"intent": "downloadFile"
}
}
π‘ Button Hierarchy
Use filled buttons for primary actions, outlined for secondary actions, and text buttons for tertiary actions. This creates a clear visual hierarchy that guides users.
π‘ Disabled State
You can dynamically disable buttons based on state: "disabled": "{{state.formInvalid}}"
Layout Components
Layout components organize other components in rows and columns. They provide flexible spacing, alignment, and responsive behavior to structure your UI.
Layout Types
layout.column- Vertical stack of componentslayout.row- Horizontal arrangement of components
Properties
| Property | Type | Description |
|---|---|---|
| type required | string | "layout.column" or "layout.row" |
| children required | ComponentNode[] | Array of child components to render |
| spacing | number | Gap between children in pixels. Default: 0 |
| padding | number | object | Padding around the layout. Number for all sides, or object: {top, right, bottom, left} |
| mainAxisAlignment | string | "start", "center", "end", "spaceBetween", "spaceAround", "spaceEvenly". Default: "start" |
| crossAxisAlignment | string | "start", "center", "end", "stretch". Default: "start" |
| width | number | string | Width in pixels or percentage (e.g., "100%") |
| height | number | string | Height in pixels or percentage |
| backgroundColor | string | Background color (hex, Material color token, or CSS color) |
| borderRadius | number | Corner radius in pixels |
| scrollable | boolean | Enable scrolling when content overflows. Default: false |
Examples
Basic Column
{
"type": "layout.column",
"padding": 16,
"spacing": 12,
"children": [
{
"type": "text",
"content": "Title",
"style": "titleLarge"
},
{
"type": "text",
"content": "Subtitle",
"style": "bodyMedium"
}
]
}
Row with Alignment
{
"type": "layout.row",
"spacing": 8,
"mainAxisAlignment": "spaceBetween",
"crossAxisAlignment": "center",
"children": [
{
"type": "text",
"content": "Total",
"style": "bodyLarge"
},
{
"type": "text",
"content": "$99.99",
"style": "titleMedium",
"fontWeight": "bold"
}
]
}
Nested Layouts
{
"type": "layout.column",
"padding": 16,
"spacing": 16,
"children": [
{
"type": "layout.row",
"spacing": 8,
"crossAxisAlignment": "center",
"children": [
{
"type": "avatar",
"url": "{{user.avatar}}",
"size": 40
},
{
"type": "layout.column",
"spacing": 4,
"children": [
{
"type": "text",
"content": "{{user.name}}",
"style": "titleMedium"
},
{
"type": "text",
"content": "{{user.email}}",
"style": "bodySmall"
}
]
}
]
}
]
}
Padding Object Syntax
{
"type": "layout.column",
"padding": {
"top": 24,
"right": 16,
"bottom": 24,
"left": 16
},
"children": [...]
}
π‘ Alignment Guide
mainAxisAlignment controls alignment along the primary direction (vertical for column, horizontal for row).
crossAxisAlignment controls alignment perpendicular to the primary direction.
π‘ Responsive Spacing
Use consistent spacing values (8, 12, 16, 24) to create a harmonious layout that follows Material Design spacing guidelines.
Card Component
Cards are surfaces that display content and actions on a single topic. They provide elevation, borders, and can be made interactive with tap handlers.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "card" | Component type identifier |
| child required | ComponentNode | Single child component to render inside the card |
| elevation | number | Shadow depth: 0-5. Default: 1 |
| padding | number | object | Internal padding. Number for all sides, or object: {top, right, bottom, left} |
| backgroundColor | string | Card background color. Default: "surface" |
| borderRadius | number | Corner radius in pixels. Default: 12 |
| onPress | Action | Action[] | Action(s) to execute when card is tapped |
| width | number | string | Card width in pixels or percentage |
| height | number | string | Card height in pixels or percentage |
Examples
Basic Card
{
"type": "card",
"elevation": 2,
"padding": 16,
"child": {
"type": "layout.column",
"spacing": 8,
"children": [
{
"type": "text",
"content": "Card Title",
"style": "titleMedium"
},
{
"type": "text",
"content": "Card content goes here",
"style": "bodyMedium"
}
]
}
}
Interactive Card
{
"type": "card",
"elevation": 1,
"padding": 16,
"onPress": {
"intent": "openDetails",
"props": {
"id": "{{item.id}}"
}
},
"child": {
"type": "layout.row",
"spacing": 12,
"crossAxisAlignment": "center",
"children": [
{
"type": "avatar",
"url": "{{item.imageUrl}}",
"size": 48
},
{
"type": "layout.column",
"spacing": 4,
"children": [
{
"type": "text",
"content": "{{item.title}}",
"style": "titleMedium"
},
{
"type": "text",
"content": "{{item.subtitle}}",
"style": "bodySmall"
}
]
}
]
}
}
Card with Image
{
"type": "card",
"elevation": 2,
"padding": 0,
"child": {
"type": "layout.column",
"spacing": 0,
"children": [
{
"type": "image",
"url": "{{product.imageUrl}}",
"height": 200,
"width": "100%",
"fit": "cover"
},
{
"type": "layout.column",
"padding": 16,
"spacing": 8,
"children": [
{
"type": "text",
"content": "{{product.name}}",
"style": "titleMedium"
},
{
"type": "text",
"content": "{{product.price}}",
"style": "bodyLarge",
"fontWeight": "bold"
}
]
}
]
}
}
π‘ Elevation Levels
Use elevation 0 for flat cards, 1 for resting state, 2-3 for hover/focus, and 4-5 for dragged states. Higher elevation creates more visual separation.
π‘ Interactive Cards
When adding onPress to a card, it automatically becomes interactive with hover and press states. Perfect for list items and navigation.
Input Component
Text input fields allow users to enter and edit text. They follow Material Design 3 text field specifications with support for labels, hints, validation, and various input types.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "input" | Component type identifier |
| statePath required | string | Path in state where value is stored (e.g., "username") |
| label | string | Floating label text |
| placeholder | string | Hint text shown when input is empty |
| helperText | string | Supporting text below input field |
| errorText | string | Error message to display. Supports {{bindings}} |
| inputType | string | "text", "email", "password", "number", "tel", "url". Default: "text" |
| maxLength | number | Maximum character length |
| multiline | boolean | Enable multi-line text area. Default: false |
| rows | number | Number of visible rows (for multiline). Default: 3 |
| disabled | boolean | Disable input interaction. Can use {{bindings}} |
| required | boolean | Mark field as required (shows * in label) |
| onChange | Action | Action[] | Action(s) to execute when value changes |
Examples
Basic Text Input
{
"type": "input",
"statePath": "username",
"label": "Username",
"placeholder": "Enter your username"
}
Email Input with Validation
{
"type": "input",
"statePath": "email",
"label": "Email Address",
"inputType": "email",
"required": true,
"errorText": "{{state.emailError}}",
"helperText": "We'll never share your email"
}
Password Input
{
"type": "input",
"statePath": "password",
"label": "Password",
"inputType": "password",
"required": true,
"minLength": 8,
"helperText": "At least 8 characters"
}
Multi-line Text Area
{
"type": "input",
"statePath": "description",
"label": "Description",
"multiline": true,
"rows": 5,
"maxLength": 500,
"helperText": "{{state.description.length || 0}}/500 characters"
}
Input with Change Handler
{
"type": "input",
"statePath": "searchQuery",
"label": "Search",
"placeholder": "Search items...",
"onChange": {
"intent": "searchItems",
"props": {
"query": "{{state.searchQuery}}"
}
}
}
π‘ State Binding
The input automatically updates the state at statePath as the user types. Access the value elsewhere with {{state.username}}.
π‘ Validation
Use errorText with state bindings to show validation errors: "errorText": "{{state.errors.email}}"
Image Component
Display images from URLs with support for sizing, object fit, and loading states. Perfect for photos, illustrations, and icons.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "image" | Component type identifier |
| url required | string | Image URL. Supports {{bindings}} |
| width | number | string | Width in pixels or percentage (e.g., "100%") |
| height | number | string | Height in pixels or percentage |
| fit | string | "cover", "contain", "fill", "none", "scaleDown". Default: "cover" |
| borderRadius | number | Corner radius in pixels |
| alt | string | Alternative text for accessibility |
| onPress | Action | Action[] | Action(s) to execute when image is tapped |
Examples
Basic Image
{
"type": "image",
"url": "https://example.com/photo.jpg",
"width": "100%",
"height": 200,
"fit": "cover",
"borderRadius": 8
}
Profile Image
{
"type": "image",
"url": "{{user.profilePhoto}}",
"width": 120,
"height": 120,
"fit": "cover",
"borderRadius": 60,
"alt": "{{user.name}} profile photo"
}
Product Image with Click
{
"type": "image",
"url": "{{product.imageUrl}}",
"width": "100%",
"height": 300,
"fit": "contain",
"onPress": {
"type": "action.overlay.open",
"overlayId": "imageGallery",
"props": {
"images": "{{product.images}}"
}
}
}
Thumbnail Image
{
"type": "image",
"url": "{{item.thumbnail}}",
"width": 80,
"height": 80,
"fit": "cover",
"borderRadius": 4
}
π‘ Object Fit Guide
cover- Fills entire area, may crop image (best for backgrounds)contain- Shows entire image, may have empty space (best for logos)fill- Stretches to fill area, may distort image
π‘ Performance
Always specify width and height to prevent layout shift when images load. Use appropriate image sizes to reduce bandwidth.
Avatar Component
Avatars display user profile images or initials in a circular format. They're commonly used in lists, headers, and user profiles.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "avatar" | Component type identifier |
| url | string | Avatar image URL. Supports {{bindings}} |
| initials | string | Initials to display if no image URL. Supports {{bindings}} |
| size | number | Avatar diameter in pixels. Default: 40 |
| backgroundColor | string | Background color for initials (hex or Material color token) |
| textColor | string | Color for initials text |
| onPress | Action | Action[] | Action(s) to execute when avatar is tapped |
Examples
Avatar with Image
{
"type": "avatar",
"url": "{{user.avatarUrl}}",
"size": 48
}
Avatar with Initials
{
"type": "avatar",
"initials": "{{user.name.substring(0, 2).toUpperCase()}}",
"size": 40,
"backgroundColor": "#6750A4",
"textColor": "#FFFFFF"
}
Large Profile Avatar
{
"type": "avatar",
"url": "{{state.currentUser.photo}}",
"size": 120,
"onPress": {
"type": "action.overlay.open",
"overlayId": "editProfile"
}
}
Small Avatar in List
{
"type": "layout.row",
"spacing": 12,
"crossAxisAlignment": "center",
"children": [
{
"type": "avatar",
"url": "{{item.userAvatar}}",
"initials": "{{item.userInitials}}",
"size": 32
},
{
"type": "text",
"content": "{{item.userName}}",
"style": "bodyMedium"
}
]
}
π‘ Fallback Behavior
If url fails to load or is empty, the avatar will display initials instead. Always provide initials as a fallback.
π‘ Size Guidelines
- 24-32px: Tiny avatars in dense lists or chips
- 40-48px: Standard list items and comments
- 64-80px: User profiles and cards
- 120px+: Large profile headers
Chip Component
Chips are compact elements used for filtering, selection, and toggling. They show selected state visually.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "chip" | Component type |
| label required | string | Text displayed on the chip |
| selected | boolean | Whether the chip is active/selected. Supports bindings: "{{state.filter == 'Design'}}" |
| onPress | Action | Action triggered when chip is clicked |
Example
{
"type": "chip",
"label": "Design",
"selected": "{{state.filter == 'Design'}}",
"onPress": {
"type": "action.state.patch",
"path": "filter",
"value": "Design"
}
}
Badge Component
Small colored labels used for status, categories, or counts.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "badge" | Component type |
| label required | string | Badge text |
| color | string | "default" | "success" | "warning" | "error" | "info" |
Example
{ "type": "badge", "label": "New", "color": "success" }
{ "type": "badge", "label": "Error", "color": "error" }
{ "type": "badge", "label": "{{item.status}}", "color": "info" }
Progress Component
Linear progress bar. Note: progress.circular and progress.linear do NOT exist β use progress.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "progress" | Component type |
| value | number | 0β100 (percentage) |
| label | string | Optional text shown next to bar |
| color | string | "primary" | "success" | "error" |
Example
{
"type": "progress",
"value": "{{item.completionPercent}}",
"label": "{{item.completionPercent}}% complete",
"color": "primary"
}
Tabs Component
Horizontal tab bar that switches between content panels. Bind activeTab to state for control.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "tabs" | Component type |
| tabs required | array | Array of { id, label, content } |
| activeTab | string | ID of the currently active tab. Use binding: "{{state.tab}}" |
| onTabChange | Action | Action triggered when a tab is clicked. Use {{tab}} for the clicked tab's id |
Example
{
"type": "tabs",
"activeTab": "{{state.tab}}",
"onTabChange": {
"type": "action.state.patch",
"path": "tab",
"value": "{{tab}}"
},
"tabs": [
{ "id": "home", "label": "Home", "content": { "type": "text", "content": "Home" } },
{ "id": "settings", "label": "Settings", "content": { "type": "text", "content": "Settings" } }
]
}
Grid Component
Static grid with fixed children. For API-driven grids, use collection with layout: "grid".
Note: layout.grid does NOT exist β use grid.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "grid" | Component type |
| columns | number | Number of columns (default 3) |
| spacing | number | Gap between items (px) |
| children | array | Child components |
Example
{
"type": "grid",
"columns": 3,
"spacing": 16,
"children": [
{ "type": "card", "child": { "type": "text", "content": "Item 1" } },
{ "type": "card", "child": { "type": "text", "content": "Item 2" } },
{ "type": "card", "child": { "type": "text", "content": "Item 3" } }
]
}
Collection Component
Collections render lists of data by repeating a template for each item. They support data binding, item actions, and automatic updates when the underlying data changes.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "collection" | Component type identifier |
| data required | string | State path to array of items (e.g., "items" or "users") |
| template required | ComponentNode | Component template to render for each item |
| spacing | number | Gap between items in pixels. Default: 0 |
| emptyState | ComponentNode | Component to show when data array is empty |
| loadingState | ComponentNode | Component to show while data is loading |
Item Context
Within the template, you can access:
{{item.propertyName}}- Properties of the current item{{index}}- Zero-based index of the current item
Examples
Basic List
{
"type": "collection",
"data": "users",
"spacing": 8,
"template": {
"type": "card",
"padding": 16,
"child": {
"type": "text",
"content": "{{item.name}}",
"style": "bodyLarge"
}
}
}
User List with Avatars
{
"type": "collection",
"data": "users",
"spacing": 12,
"template": {
"type": "card",
"padding": 16,
"onPress": {
"intent": "viewProfile",
"props": {
"userId": "{{item.id}}"
}
},
"child": {
"type": "layout.row",
"spacing": 12,
"crossAxisAlignment": "center",
"children": [
{
"type": "avatar",
"url": "{{item.avatar}}",
"initials": "{{item.initials}}",
"size": 48
},
{
"type": "layout.column",
"spacing": 4,
"children": [
{
"type": "text",
"content": "{{item.name}}",
"style": "titleMedium"
},
{
"type": "text",
"content": "{{item.email}}",
"style": "bodySmall",
"color": "#79747E"
}
]
}
]
}
}
}
Product Grid with Empty State
{
"type": "collection",
"data": "products",
"spacing": 16,
"emptyState": {
"type": "layout.column",
"padding": 32,
"spacing": 16,
"mainAxisAlignment": "center",
"crossAxisAlignment": "center",
"children": [
{
"type": "text",
"content": "No products found",
"style": "titleMedium"
},
{
"type": "text",
"content": "Try adjusting your filters",
"style": "bodyMedium",
"color": "#79747E"
}
]
},
"template": {
"type": "card",
"elevation": 2,
"child": {
"type": "layout.column",
"spacing": 12,
"children": [
{
"type": "image",
"url": "{{item.imageUrl}}",
"width": "100%",
"height": 200,
"fit": "cover"
},
{
"type": "layout.column",
"padding": 16,
"spacing": 8,
"children": [
{
"type": "text",
"content": "{{item.name}}",
"style": "titleMedium"
},
{
"type": "text",
"content": "${{item.price}}",
"style": "bodyLarge",
"fontWeight": "bold",
"color": "#6750A4"
}
]
}
]
}
}
}
List with Index
{
"type": "collection",
"data": "tasks",
"spacing": 4,
"template": {
"type": "layout.row",
"spacing": 8,
"padding": 12,
"children": [
{
"type": "text",
"content": "{{index + 1}}.",
"style": "bodyMedium",
"color": "#79747E"
},
{
"type": "text",
"content": "{{item.title}}",
"style": "bodyMedium"
}
]
}
}
π‘ Data Reactivity
Collections automatically update when the data array changes. Add, remove, or modify items using action.state.patch and the UI updates instantly.
π‘ Performance
Collections use virtualization for large lists, rendering only visible items. This ensures smooth scrolling even with thousands of items.
Collection Map Component
Renders an array already in state β no HTTP call needed. Use when data is already loaded.
Use collection when data comes from an API, use collection.map when data is already in state.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "collection.map" | Component type |
| data required | string (binding) | Array expression. Full JS supported: "{{state.items.filter(...)}}" |
| template or itemTemplate | Component | Template rendered for each item. {{item}} and {{index}} available |
| layout | string | "list" (default) | "grid" |
| columns | number | Grid columns (default 3) |
| spacing | number | Gap between items (px) |
Filtering & Sorting
The data expression re-evaluates on every state change. Put all filtering/sorting inline in the expression:
"data": "{{state.items.filter(i => i.active)}}"
"data": "{{state.filter == 'all' ? state.items : state.items.filter(i => i.type == state.filter)}}"
"data": "{{state.items.sort((a, b) => a.name.localeCompare(b.name))}}"
"data": "{{state.items.slice(0, state.limit)}}"
β οΈ Common Mistake
If you patch state.filter and want the list to update, the filter must be in the data expression. Simply patching state won't re-filter a static data array.
Example
{
"state": { "filter": "all", "items": [...] },
"screen": {
"type": "layout.column",
"children": [
{
"type": "layout.row",
"spacing": 8,
"children": [
{ "type": "chip", "label": "All", "selected": "{{state.filter == 'all'}}",
"onPress": { "type": "action.state.patch", "path": "filter", "value": "all" } },
{ "type": "chip", "label": "Active", "selected": "{{state.filter == 'active'}}",
"onPress": { "type": "action.state.patch", "path": "filter", "value": "active" } }
]
},
{
"type": "collection.map",
"data": "{{state.filter == 'all' ? state.items : state.items.filter(i => i.status == state.filter)}}",
"spacing": 8,
"template": {
"type": "card",
"child": { "type": "text", "content": "{{item.title}}" }
}
}
]
}
}
Conditional Render
Conditionally show or hide components based on state values. Perfect for showing loading states, error messages, or content that depends on user permissions.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "conditional" | Component type identifier |
| condition required | string | Expression to evaluate (e.g., "{{state.isLoggedIn}}") |
| child required | ComponentNode | Component to render when condition is true |
| fallback | ComponentNode | Component to render when condition is false |
Examples
Show/Hide Based on State
{
"type": "conditional",
"condition": "{{state.isLoggedIn}}",
"child": {
"type": "text",
"content": "Welcome, {{state.user.name}}!",
"style": "titleMedium"
},
"fallback": {
"type": "button.filled",
"label": "Login",
"onPress": {
"type": "action.overlay.open",
"overlayId": "loginModal"
}
}
}
Loading State
{
"type": "conditional",
"condition": "{{state.loading}}",
"child": {
"type": "text",
"content": "Loading...",
"style": "bodyMedium"
},
"fallback": {
"type": "collection",
"data": "items",
"template": {...}
}
}
Error Handling
{
"type": "conditional",
"condition": "{{state.error}}",
"child": {
"type": "card",
"backgroundColor": "#FEE2E2",
"padding": 16,
"child": {
"type": "text",
"content": "Error: {{state.error}}",
"color": "#BA1A1A"
}
},
"fallback": {
"type": "text",
"content": "{{state.data}}",
"style": "bodyMedium"
}
}
Comparison Condition
{
"type": "conditional",
"condition": "{{state.cart.items.length > 0}}",
"child": {
"type": "button.filled",
"label": "Checkout ({{state.cart.items.length}} items)",
"onPress": {
"intent": "checkout"
}
},
"fallback": {
"type": "text",
"content": "Your cart is empty",
"style": "bodyMedium",
"color": "#79747E"
}
}
Multiple Conditions
{
"type": "conditional",
"condition": "{{state.user.role === 'admin'}}",
"child": {
"type": "button.filled",
"label": "Admin Panel",
"icon": "settings",
"onPress": {
"intent": "openAdminPanel"
}
}
}
π‘ Condition Expressions
Conditions support JavaScript expressions:
- Boolean:
{{state.isActive}} - Comparison:
{{state.count > 10}} - Equality:
{{state.status === 'complete'}} - Logical:
{{state.a && state.b}} - Negation:
{{!state.isEmpty}}
π‘ Nested Conditionals
You can nest conditional components to handle complex scenarios with multiple states.
HTTP Request Action
Make HTTP requests to REST APIs. Supports GET, POST, PUT, DELETE methods with headers, request bodies, and automatic state mapping of responses.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "action.http" | Action type identifier |
| method required | string | "GET", "POST", "PUT", "PATCH", "DELETE" |
| url required | string | API endpoint URL. Supports {{bindings}} |
| headers | object | HTTP headers as key-value pairs |
| body | object | Request body (for POST, PUT, PATCH). Supports {{bindings}} |
| onSuccess | Action | Action executed after a successful response. Use {{response}} to access the full response body |
| onError | Action | Action[] | Actions to execute on error response (non-2xx status) |
Examples
GET Request
{
"type": "action.http",
"method": "GET",
"url": "https://api.example.com/users",
"onSuccess": {
"type": "action.state.patch",
"path": "users",
"value": "{{response}}"
}
}
POST Request with Body
{
"type": "action.http",
"method": "POST",
"url": "https://api.example.com/users",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer {{state.authToken}}"
},
"body": {
"name": "{{state.form.name}}",
"email": "{{state.form.email}}",
"role": "user"
},
"onSuccess": [
{
"type": "action.snackbar.show",
"message": "User created successfully!"
},
{
"intent": "loadUsers"
}
],
"onError": {
"type": "action.snackbar.show",
"message": "Error creating user",
"severity": "error"
}
}
DELETE Request
{
"type": "action.http",
"method": "DELETE",
"url": "https://api.example.com/users/{{props.userId}}",
"headers": {
"Authorization": "Bearer {{state.authToken}}"
},
"onSuccess": {
"intent": "refreshUserList"
}
}
PUT Request with onSuccess
{
"type": "action.http",
"method": "PUT",
"url": "https://api.example.com/profile",
"body": {
"name": "{{state.profile.name}}",
"bio": "{{state.profile.bio}}"
},
"onSuccess": {
"type": "action.sequence",
"actions": [
{ "type": "action.state.patch", "path": "profile", "value": "{{response}}" },
{ "type": "action.snackbar.show", "message": "Profile updated!" }
]
}
}
Search with Dynamic URL
{
"type": "action.http",
"method": "GET",
"url": "https://api.example.com/search?q={{state.searchQuery}}&limit=20",
"onSuccess": {
"type": "action.state.patch",
"path": "searchResults",
"value": "{{response}}"
}
}
π‘ onSuccess & {{response}}
Use onSuccess to handle the API response. {{response}} is the full response body and is only available inside onSuccess.
π‘ Error Handling
Always provide onError handlers to gracefully handle API failures and show user-friendly error messages.
State Patch Action
Update application state reactively. When state changes, all components that reference the updated values automatically re-render.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "action.state.patch" | Action type identifier |
| path required | string | State path to update (e.g., "counter", "user.name", "items[0].status") |
| value required | any | New value. Supports {{bindings}} and expressions |
Examples
Update Simple Value
{
"type": "action.state.patch",
"path": "counter",
"value": "{{state.counter + 1}}"
}
Update Nested Property
{
"type": "action.state.patch",
"path": "user.settings.darkMode",
"value": true
}
Update from Input
{
"type": "action.state.patch",
"path": "form.email",
"value": "{{state.email}}"
}
Toggle Boolean
{
"type": "action.state.patch",
"path": "isExpanded",
"value": "{{!state.isExpanded}}"
}
Update Array Element
{
"type": "action.state.patch",
"path": "todos[{{index}}].completed",
"value": true
}
Set Multiple Values
[
{
"type": "action.state.patch",
"path": "loading",
"value": false
},
{
"type": "action.state.patch",
"path": "error",
"value": null
},
{
"type": "action.state.patch",
"path": "data",
"value": "{{response}}"
}
]
Update with Calculation
{
"type": "action.state.patch",
"path": "cart.total",
"value": "{{state.cart.items.reduce((sum, item) => sum + item.price, 0)}}"
}
π‘ Dot Notation
Use dot notation to update nested properties: "user.profile.name". PineUI will create missing intermediate objects automatically.
π‘ Expressions
The value field supports JavaScript expressions. You can perform calculations, concatenations, and transformations.
Overlay Control Actions
Open and close modals, dialogs, and bottom sheets. Overlays are defined in the schema's
overlays section and controlled with these actions.
action.overlay.open
| Property | Type | Description |
|---|---|---|
| type required | "action.overlay.open" | Action type identifier |
| overlayId required | string | ID of the overlay to open (defined in overlays section) |
| props | object | Data to pass to the overlay. Accessible via {{props.key}} |
action.overlay.close
| Property | Type | Description |
|---|---|---|
| type required | "action.overlay.close" | Action type identifier |
| overlayId | string | Specific overlay to close. If omitted, closes the topmost overlay |
Examples
Open Modal
{
"type": "action.overlay.open",
"overlayId": "confirmDialog",
"props": {
"title": "Delete Item",
"message": "Are you sure you want to delete this item?",
"itemId": "{{item.id}}"
}
}
Close Current Overlay
{
"type": "action.overlay.close"
}
Close Specific Overlay
{
"type": "action.overlay.close",
"overlayId": "editModal"
}
Open Then Close After Action
[
{
"type": "action.http",
"method": "POST",
"url": "/api/save",
"body": "{{state.formData}}"
},
{
"type": "action.overlay.close"
},
{
"type": "action.snackbar.show",
"message": "Saved successfully!"
}
]
Overlay Definition Example
{
"overlays": {
"confirmDialog": {
"type": "dialog",
"content": {
"type": "layout.column",
"padding": 24,
"spacing": 16,
"children": [
{
"type": "text",
"content": "{{props.title}}",
"style": "titleLarge"
},
{
"type": "text",
"content": "{{props.message}}",
"style": "bodyMedium"
},
{
"type": "layout.row",
"spacing": 8,
"mainAxisAlignment": "end",
"children": [
{
"type": "button.text",
"label": "Cancel",
"onPress": {
"type": "action.overlay.close"
}
},
{
"type": "button.filled",
"label": "Delete",
"onPress": [
{
"intent": "deleteItem",
"props": {
"id": "{{props.itemId}}"
}
},
{
"type": "action.overlay.close"
}
]
}
]
}
]
}
}
}
}
π‘ Props Passing
Use props to pass data to overlays. Access inside the overlay with {{props.propertyName}}.
π‘ Auto Close
Calling action.overlay.close without an ID closes the most recently opened overlay, perfect for "Cancel" buttons.
Snackbar Action
Display brief messages at the bottom of the screen. Snackbars (also called toasts) are perfect for confirmation messages, errors, and notifications.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "action.snackbar.show" | Action type identifier |
| message required | string | Message to display. Supports {{bindings}} |
| duration | number | Duration in milliseconds. Default: 3000 |
| severity | string | "success", "error", "warning", "info". Default: "info" |
| action | object | Optional action button with label and onPress |
Examples
Simple Snackbar
{
"type": "action.snackbar.show",
"message": "Item added to cart"
}
Success Message
{
"type": "action.snackbar.show",
"message": "Profile updated successfully!",
"severity": "success",
"duration": 2000
}
Error Message
{
"type": "action.snackbar.show",
"message": "Failed to save changes",
"severity": "error",
"duration": 5000
}
With Action Button
{
"type": "action.snackbar.show",
"message": "Item deleted",
"severity": "info",
"action": {
"label": "Undo",
"onPress": {
"intent": "undoDelete"
}
}
}
Dynamic Message
{
"type": "action.snackbar.show",
"message": "{{state.items.length}} items in your cart",
"duration": 2000
}
Warning Message
{
"type": "action.snackbar.show",
"message": "You have unsaved changes",
"severity": "warning",
"duration": 4000
}
π‘ Severity Colors
success- Green background, for completed actionserror- Red background, for failureswarning- Orange background, for cautionsinfo- Blue/gray background, for neutral messages
π‘ Duration Guidelines
Keep messages brief and readable. Use 2-3 seconds for success, 4-5 seconds for errors users need to read carefully.
Sequence Action
Run multiple actions in order, one after another. Each action awaits completion before the next runs. Essential for multi-step workflows.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "action.sequence" | Action type |
| actions required | Action[] | Array of actions to run in order |
Example
{
"type": "action.sequence",
"actions": [
{ "type": "action.state.patch", "path": "loading", "value": true },
{ "type": "action.http", "method": "POST", "url": "/api/save",
"body": { "text": "{{state.text}}" } },
{ "type": "action.state.patch", "path": "loading", "value": false },
{ "type": "action.overlay.close", "overlayId": "editModal" },
{ "type": "action.snackbar.show", "message": "Saved!" }
]
}
Delay Action
Add a time delay before executing the next action in a sequence. Useful for creating animations, debouncing, or timing-based workflows.
Properties
| Property | Type | Description |
|---|---|---|
| type required | "action.delay" | Action type identifier |
| duration required | number | Delay duration in milliseconds |
Examples
Simple Delay
[
{
"type": "action.snackbar.show",
"message": "Processing..."
},
{
"type": "action.delay",
"duration": 1000
},
{
"type": "action.snackbar.show",
"message": "Complete!",
"severity": "success"
}
]
Loading State with Delay
[
{
"type": "action.state.patch",
"path": "loading",
"value": true
},
{
"type": "action.http",
"method": "GET",
"url": "/api/data"
},
{
"type": "action.delay",
"duration": 500
},
{
"type": "action.state.patch",
"path": "loading",
"value": false
}
]
Debounced Search
{
"onChange": [
{
"type": "action.state.patch",
"path": "searchQuery",
"value": "{{state.input}}"
},
{
"type": "action.delay",
"duration": 300
},
{
"intent": "performSearch"
}
]
}
π‘ Action Sequences
Delays are most useful in action arrays where you need to pace operations. Without delay, all actions execute immediately.
π‘ User Experience
Use delays sparingly. Long delays can make your app feel sluggish. Most delays should be 100-500ms.
Custom Components
Create reusable component templates that can be instantiated multiple times with different properties. Custom components help you build a consistent design system and reduce duplication in your schemas.
Defining Custom Components
Custom components are defined in the schema's components section. The key must start with component. and the structure uses "definition":
{
"components": {
"component.userRow": {
"definition": {
"type": "layout.row",
"spacing": 12,
"children": [
{
"type": "avatar",
"src": "{{props.user.avatar}}",
"size": 40
},
{
"type": "layout.column",
"spacing": 2,
"children": [
{ "type": "text", "content": "{{props.user.name}}", "style": "titleSmall" },
{ "type": "text", "content": "{{props.user.email}}", "style": "bodySmall" }
]
}
]
}
}
}
}
Using Custom Components
Use the component name as the type, and pass data via props:
{
"type": "component.userRow",
"props": {
"user": "{{item}}"
}
}
β οΈ Important Rules
- Component names must start with
component.(e.g.component.postCard) - The structure key is
"definition", not"template" - Inside the definition, access passed props via
{{props.fieldName}} - Props can be nested:
{{props.user.name}}
Examples
Product Card Component
{
"components": {
"component.productCard": {
"definition": {
"type": "card",
"elevation": 1,
"padding": 0,
"onPress": { "intent": "product.open", "productId": "{{item.id}}" },
"child": {
"type": "layout.column",
"spacing": 0,
"children": [
{
"type": "image",
"src": "{{props.product.image}}",
"aspectRatio": 1.5,
"borderRadius": 12
},
{
"type": "layout.column",
"padding": 12,
"spacing": 4,
"children": [
{ "type": "text", "content": "{{props.product.name}}", "style": "titleSmall" },
{ "type": "text", "content": "${{props.product.price}}", "style": "bodyMedium", "color": "#6750A4" }
]
}
]
}
}
}
}
}
Using Product Card in a Collection
{
"type": "collection",
"layout": "grid",
"columns": 3,
"data": { "type": "action.http", "method": "GET", "url": "/api/products" },
"itemTemplate": {
"type": "component.productCard",
"props": { "product": "{{item}}" }
}
}
π‘ Props Access
Inside custom component templates, access passed properties with {{props.propertyName}}. This keeps components reusable and flexible.
π‘ Component Library
Build a library of custom components for your app: buttons, cards, headers, etc. This ensures visual consistency and speeds up development.
π‘ Default Values
Use the || operator for default prop values: "{{props.color || '#6750A4'}}"
Overlays & Modals
Overlays display content above the main screen. PineUI supports three overlay types: dialogs (centered modals), bottom sheets (slide-up panels), and modals (full-screen overlays).
Overlay Types
dialog- Centered modal for confirmations and formsbottomSheet- Slide-up panel from the bottommodal- Full-screen overlay for complex workflows
Overlay Properties
| Property | Type | Description |
|---|---|---|
| type required | string | "dialog", "bottomSheet", or "modal" |
| content required | ComponentNode | Component to render inside the overlay |
| dismissible | boolean | Allow closing by tapping outside or back button. Default: true |
| maxWidth | number | Maximum width in pixels (dialog only) |
Examples
Confirm Dialog
{
"overlays": {
"deleteConfirm": {
"type": "dialog",
"maxWidth": 400,
"content": {
"type": "layout.column",
"padding": 24,
"spacing": 20,
"children": [
{
"type": "text",
"content": "Delete Item?",
"style": "titleLarge"
},
{
"type": "text",
"content": "This action cannot be undone.",
"style": "bodyMedium",
"color": "#49454F"
},
{
"type": "layout.row",
"spacing": 12,
"mainAxisAlignment": "end",
"children": [
{
"type": "button.text",
"label": "Cancel",
"onPress": {
"type": "action.overlay.close"
}
},
{
"type": "button.filled",
"label": "Delete",
"color": "#BA1A1A",
"onPress": [
{
"intent": "deleteItem"
},
{
"type": "action.overlay.close"
}
]
}
]
}
]
}
}
}
}
Bottom Sheet Menu
{
"overlays": {
"optionsMenu": {
"type": "bottomSheet",
"content": {
"type": "layout.column",
"padding": 16,
"spacing": 8,
"children": [
{
"type": "text",
"content": "Options",
"style": "titleMedium",
"padding": 8
},
{
"type": "button.text",
"label": "Share",
"icon": "share",
"fullWidth": true,
"onPress": {
"intent": "shareItem"
}
},
{
"type": "button.text",
"label": "Edit",
"icon": "edit",
"fullWidth": true,
"onPress": {
"intent": "editItem"
}
},
{
"type": "button.text",
"label": "Delete",
"icon": "delete",
"fullWidth": true,
"color": "#BA1A1A",
"onPress": {
"type": "action.overlay.open",
"overlayId": "deleteConfirm"
}
}
]
}
}
}
}
Form Modal
{
"overlays": {
"createUserModal": {
"type": "modal",
"dismissible": false,
"content": {
"type": "layout.column",
"padding": 24,
"spacing": 20,
"height": "100%",
"children": [
{
"type": "layout.row",
"mainAxisAlignment": "spaceBetween",
"crossAxisAlignment": "center",
"children": [
{
"type": "text",
"content": "Create User",
"style": "titleLarge"
},
{
"type": "button.icon",
"icon": "close",
"onPress": {
"type": "action.overlay.close"
}
}
]
},
{
"type": "layout.column",
"spacing": 16,
"children": [
{
"type": "input",
"statePath": "newUser.name",
"label": "Full Name",
"required": true
},
{
"type": "input",
"statePath": "newUser.email",
"label": "Email",
"inputType": "email",
"required": true
},
{
"type": "input",
"statePath": "newUser.role",
"label": "Role"
}
]
},
{
"type": "layout.row",
"spacing": 12,
"mainAxisAlignment": "end",
"children": [
{
"type": "button.outlined",
"label": "Cancel",
"onPress": {
"type": "action.overlay.close"
}
},
{
"type": "button.filled",
"label": "Create",
"onPress": [
{
"intent": "createUser"
},
{
"type": "action.overlay.close"
}
]
}
]
}
]
}
}
}
}
Opening an Overlay
{
"type": "button.filled",
"label": "New User",
"icon": "add",
"onPress": {
"type": "action.overlay.open",
"overlayId": "createUserModal"
}
}
π‘ When to Use Each Type
- Dialog: Quick confirmations, alerts, simple forms
- Bottom Sheet: Action menus, filters, settings
- Modal: Multi-step forms, detailed views, complex workflows
π‘ Passing Data
Use the props parameter in action.overlay.open to pass data to overlays. Access it in the overlay with {{props.propertyName}}.
Design System
PineUI uses Material Design 3 as its foundation, providing a comprehensive design system with customizable colors, typography, spacing, and components.
Color System
PineUI supports Material Design 3 color tokens. You can customize colors at the component level using standard color values (hex, rgb, named colors) or Material Design 3 semantic tokens.
Common Color Properties
{
"type": "text",
"content": "Custom colored text",
"color": "#6750A4", // Hex color
"backgroundColor": "#F7F2FA" // Background color
}
Material Design 3 Semantic Colors
Primary Colors
#6750A4- Primary#E8DEF8- Primary Container#FFFFFF- On Primary#21005D- On Primary Container
Secondary Colors
#625B71- Secondary#E8DEF8- Secondary Container#FFFFFF- On Secondary#1D192B- On Secondary Container
Surface & Background
#FFFBFE- Surface / Background#1C1B1F- On Surface#E7E0EC- Surface Variant#49454F- On Surface Variant#CAC4D0- Outline
Error & Status
#BA1A1A- Error#F9DEDC- Error Container#FFFFFF- On Error#410E0B- On Error Container
Example: Custom Button Colors
{
"type": "button.filled",
"label": "Custom Button",
"backgroundColor": "#6750A4",
"color": "#FFFFFF"
}
Typography
PineUI implements Material Design 3 type scale with predefined text styles. Each style has specific size, weight, and line height optimized for readability.
Display Styles (Large Headlines)
{
"type": "text",
"content": "Display Text",
"style": "displayLarge" // 57sp, Regular
// or "displayMedium" // 45sp, Regular
// or "displaySmall" // 36sp, Regular
}
Headline Styles (Section Headers)
{
"type": "text",
"content": "Headline",
"style": "headlineLarge" // 32sp, Regular
// or "headlineMedium" // 28sp, Regular
// or "headlineSmall" // 24sp, Regular
}
Title Styles (Emphasis)
{
"type": "text",
"content": "Title",
"style": "titleLarge" // 22sp, Regular
// or "titleMedium" // 16sp, Medium (500)
// or "titleSmall" // 14sp, Medium (500)
}
Body Styles (Content)
{
"type": "text",
"content": "Body text for reading",
"style": "bodyLarge" // 16sp, Regular
// or "bodyMedium" // 14sp, Regular (default)
// or "bodySmall" // 12sp, Regular
}
Label Styles (UI Elements)
{
"type": "text",
"content": "Label",
"style": "labelLarge" // 14sp, Medium (500)
// or "labelMedium" // 12sp, Medium (500)
// or "labelSmall" // 11sp, Medium (500)
}
π‘ Typography Best Practices
- Use displayLarge for hero sections and main titles
- Use headlineMedium for page titles
- Use titleMedium for card titles and list headers
- Use bodyMedium for main content (default)
- Use labelMedium for buttons and small UI elements
Spacing & Layout
PineUI uses an 8-point grid system for consistent spacing. All spacing values should be multiples of 4 (4, 8, 12, 16, 20, 24, 32, 40, 48, etc.) for visual harmony.
Common Spacing Values
4px- Extra small (tight spacing)8px- Small (compact elements)12px- Medium-small (list items)16px- Medium (default padding)24px- Large (section spacing)32px- Extra large (major sections)48px- XXL (hero spacing)
Layout Spacing Example
{
"type": "layout.column",
"padding": 16, // Internal padding (all sides)
"spacing": 12, // Space between children
"children": [
{
"type": "text",
"content": "Item 1"
},
{
"type": "text",
"content": "Item 2"
}
]
}
Card Spacing Example
{
"type": "card",
"padding": 16, // Internal padding
"child": {
"type": "layout.column",
"spacing": 8, // Tight spacing for card content
"children": [...]
}
}
Customizing Theme
You can customize the entire theme by overriding CSS variables or by setting colors directly on components.
Method 1: CSS Variables (Global Theme)
<style>
:root {
--pineui-primary: #6750A4;
--pineui-primary-container: #E8DEF8;
--pineui-on-primary: #FFFFFF;
--pineui-secondary: #625B71;
--pineui-surface: #FFFBFE;
--pineui-on-surface: #1C1B1F;
--pineui-outline: #CAC4D0;
--pineui-error: #BA1A1A;
}
</style>
Method 2: Component-Level Customization
{
"type": "button.filled",
"label": "Custom Button",
"backgroundColor": "#1976D2", // Custom blue
"color": "#FFFFFF"
}
Method 3: Custom CSS Classes
You can add custom CSS classes to any component using the className property:
// In your schema:
{
"type": "card",
"className": "my-custom-card",
"child": {...}
}
// In your CSS:
.my-custom-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 2px solid #6750A4;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}
Dark Mode Support
PineUI components automatically adapt to dark mode when you define dark theme colors. Use CSS media queries or theme switching logic:
<style>
/* Light theme (default) */
:root {
--pineui-surface: #FFFBFE;
--pineui-on-surface: #1C1B1F;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root {
--pineui-surface: #1C1B1F;
--pineui-on-surface: #E6E1E5;
--pineui-primary: #D0BCFF;
--pineui-outline: #938F99;
}
}
</style>
Responsive Design
PineUI components are responsive by default. Use layout components to create adaptive designs:
Flexible Layouts
{
"type": "layout.row",
"mainAxisAlignment": "spaceBetween", // space-between, center, start, end
"crossAxisAlignment": "center", // center, start, end, stretch
"wrap": true, // Wrap to next line on small screens
"children": [...]
}
Responsive Card Grid
{
"type": "layout.row",
"wrap": true,
"spacing": 16,
"children": [
{
"type": "card",
"padding": 16,
"child": {...}
},
{
"type": "card",
"padding": 16,
"child": {...}
}
]
}
π‘ Design Tips
- Consistency: Stick to the 8-point grid for spacing
- Hierarchy: Use typography scale to establish visual hierarchy
- Contrast: Ensure sufficient contrast between text and background (WCAG AA minimum)
- Touch Targets: Minimum 48px height for buttons on mobile
- Whitespace: Don't be afraid of empty space - it improves readability
Material Design Resources
For more information about Material Design 3, visit:
Data Bindings
Data bindings connect your UI to dynamic values using the {{}} syntax.
Bindings automatically update when the underlying data changes, creating a reactive UI.
Binding Types
{{state.propertyName}}- Access application state{{props.propertyName}}- Access component/overlay props{{item.propertyName}}- Access collection item data{{index}}- Access collection item index{{response}}- Access HTTP response (only available insidecollection.data.onSuccess)
State Bindings
{
"state": {
"username": "John",
"count": 42,
"user": {
"name": "Jane",
"email": "jane@example.com"
}
}
}
// Usage:
{
"type": "text",
"content": "Hello, {{state.username}}!"
}
{
"type": "text",
"content": "Count: {{state.count}}"
}
{
"type": "text",
"content": "{{state.user.name}} ({{state.user.email}})"
}
Props Bindings
In custom components (prefix must be component.):
{
"type": "component.userCard",
"props": {
"user": "{{item}}"
}
}
// Inside component definition:
{
"type": "text",
"content": "{{props.user.name}} - {{props.user.role}}"
}
Collection Item Bindings
{
"type": "collection",
"data": { "type": "action.http", "method": "GET", "url": "/api/products" },
"itemTemplate": {
"type": "card",
"child": {
"type": "layout.column",
"spacing": 8,
"children": [
{
"type": "text",
"content": "#{{index + 1}}: {{item.name}}"
},
{
"type": "text",
"content": "Price: ${{item.price}}"
}
]
}
}
}
Expressions
Bindings support JavaScript expressions:
// Math
"{{state.count + 10}}"
"{{state.price * 1.1}}"
// String operations
"{{state.firstName + ' ' + state.lastName}}"
"{{state.text.toUpperCase()}}"
// Conditionals
"{{state.count > 10 ? 'High' : 'Low'}}"
"{{state.user.role === 'admin' ? 'Administrator' : 'User'}}"
// Boolean logic
"{{state.isActive && state.isVerified}}"
"{{!state.isEmpty}}"
// Array operations
"{{state.items.length}}"
"{{state.users.map(u => u.name).join(', ')}}"
// Object access
"{{state.settings.theme || 'light'}}"
Examples
Dynamic Text
{
"type": "text",
"content": "You have {{state.notifications.length}} new notifications",
"style": "bodyMedium"
}
Conditional Styling
{
"type": "text",
"content": "{{state.status}}",
"color": "{{state.status === 'active' ? '#00695C' : '#BA1A1A'}}"
}
Dynamic Button State
{
"type": "button.filled",
"label": "{{state.loading ? 'Loading...' : 'Submit'}}",
"disabled": "{{state.loading || !state.formValid}}"
}
URL Construction
{
"type": "action.http",
"method": "GET",
"url": "https://api.example.com/users/{{state.selectedUserId}}/profile"
}
Complex Expression
{
"type": "text",
"content": "Total: ${{state.cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0).toFixed(2)}}"
}
π‘ Reactivity
When state changes, all components using bindings to that state automatically update. No manual refresh needed!
π‘ Type Safety
Bindings are evaluated at runtime. Use safe navigation: {{state.user?.name || 'Guest'}} to avoid errors when data is missing.
π‘ Performance
Keep expressions simple. Complex calculations in bindings that appear many times (like in collections) can impact performance.
License
PineUI is licensed under Apache License 2.0 with Commons Clause.
π What does this mean?
You CAN:
- β Use PineUI for free in your projects (commercial or non-commercial)
- β Modify the source code
- β Distribute copies of PineUI
- β Use it in proprietary software
You CANNOT:
- β Sell PineUI itself as a product or service
- β Offer hosting or consulting services where the primary value comes from PineUI
The Commons Clause prevents companies from taking this open-source project and selling it as a competing commercial product, while keeping it free for everyone to use in their own applications.
Full License Text
Apache License 2.0 with Commons Clause
License Agreement
PineUI - Server-Driven UI for AI-Native Applications
Copyright (c) 2026 Luma Ventures Ltda (CNPJ: 21.951.820/0001-39)
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
---
"Commons Clause" License Condition v1.0
The Software is provided to you by the Licensor under the License, as defined
below, subject to the following condition.
Without limiting other conditions in the License, the grant of rights under the
License will not include, and the License does not grant to you, the right to
Sell the Software.
For purposes of the foregoing, "Sell" means practicing any or all of the rights
granted to you under the License to provide to third parties, for a fee or
other consideration (including without limitation fees for hosting or
consulting/support services related to the Software), a product or service
whose value derives, entirely or substantially, from the functionality of the
Software. Any license notice or attribution required by the License must also
include this Commons Clause License Condition notice.
Software: PineUI
License: Apache 2.0
Licensor: Luma Ventures Ltda
CNPJ: 21.951.820/0001-39
---
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the
copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other
entities that control, are controlled by, or are under common control with
that entity. For the purposes of this definition, "control" means (i) the
power, direct or indirect, to cause the direction or management of such
entity, whether by contract or otherwise, or (ii) ownership of fifty
percent (50%) or more of the outstanding shares, or (iii) beneficial
ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation source,
and configuration files.
"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled
object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that
is included in or attached to the work.
"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial
revisions, annotations, elaborations, or other modifications represent, as
a whole, an original work of authorship. For the purposes of this License,
Derivative Works shall not include works that remain separable from, or
merely link (or bind by name) to the interfaces of, the Work and Derivative
Works thereof.
"Contribution" shall mean any work of authorship, including the original
version of the Work and any modifications or additions to that Work or
Derivative Works thereof, that is intentionally submitted to Licensor for
inclusion in the Work by the copyright owner or by an individual or Legal
Entity authorized to submit on behalf of the copyright owner.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and
such Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable patent license to make, have made, use, offer to sell, sell,
import, and otherwise transfer the Work.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works
thereof in any medium, with or without modifications, and in Source or
Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a
copy of this License; and
(b) You must cause any modified files to carry prominent notices stating
that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You
distribute, all copyright, patent, trademark, and attribution notices
from the Source form of the Work; and
(d) You must include a copy of this License and the Commons Clause restriction.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally
submitted for inclusion in the Work by You to the Licensor shall be under
the terms and conditions of this License, without any additional terms or
conditions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides
the Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied, including, without limitation, any warranties or
conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE.
8. Limitation of Liability.
In no event and under no legal theory shall any Contributor be liable to You
for damages, including any direct, indirect, special, incidental, or
consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License.
However, in accepting such obligations, You may act only on Your own behalf
and on Your sole responsibility.
---
For commercial licensing inquiries, please contact:
Luma Ventures Ltda
CNPJ: 21.951.820/0001-39
Email: wupsbr@gmail.com