Getting Started
Theory is important, but there's no substitute for hands-on experience. In this chapter, you'll set up your development environment and build your first LARC application. By the end, you'll have a working project and understand the basic development workflow.
Setting Up Your Development Environment
One of LARC's strengths is minimal setup requirements. You don't need complex tooling or configuration—just a browser, a text editor, and a way to serve files.
Requirements
Essential:- Modern browser — Chrome, Firefox, Safari, or Edge (latest version)
- Text editor — VS Code, Sublime Text, Atom, or any editor you prefer
- Local web server — Python's SimpleHTTPServer, Node's
http-server, or VS Code's Live Server extension
- VS Code with the LARC extension for snippets and IntelliSense
- Browser DevTools familiarity for debugging
- Git for version control
Quick Start with create-larc-app
The fastest way to start is using the LARC CLI:
# Install globally
npm install -g create-larc-app
# Create a new project
create-larc-app my-first-app
# Start development server
cd my-first-app
larc dev
Open http://localhost:3000 and you'll see your new LARC application running.
Manual Setup (No CLI)
Don't want to install Node.js? You can set up a LARC project manually:
1. Create project structure:mkdir my-first-app
cd my-first-app
mkdir src
mkdir src/components
mkdir public
2. Create index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My First LARC App</title>
<!-- Import Map for dependencies -->
<script type="importmap">
{
"imports": {
"@larcjs/core": "https://cdn.jsdelivr.net/npm/@larcjs/core@3.0.1/pan.mjs"
}
}
</script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background: #f5f5f5;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="src/app.js"></script>
</body>
</html>
3. Create src/app.js:
import { pan } from '@larcjs/core';
// Import your components
import './components/hello-world.js';
// Initialize app
console.log('LARC app initialized');
pan.publish('app.ready');
// Add component to page
document.getElementById('app').innerHTML = '<hello-world></hello-world>';
4. Create src/components/hello-world.js:
class HelloWorld extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div style="
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
">
<h1>Hello, LARC!</h1>
<p>Welcome to your first LARC application.</p>
</div>
`;
}
}
customElements.define('hello-world', HelloWorld);
5. Serve the files:
# Python 3
python3 -m http.server 3000
# Or Python 2
python -m SimpleHTTPServer 3000
# Or with Node.js
npx http-server -p 3000
# Or use VS Code Live Server extension
# (right-click index.html → "Open with Live Server")
Open http://localhost:3000 and you should see "Hello, LARC!" displayed.
Development Tools
#### VS Code Extensions
Install these extensions for the best experience:
LARC Extension:- Snippets for components and PAN patterns
- IntelliSense for LARC APIs
- Commands for creating components
- Auto-reload when files change
- Simple local web server
- Right-click HTML file to start
- Syntax highlighting for template literals
- Makes component templates more readable
#### Browser DevTools
Learn these DevTools features for LARC development:
Elements Panel:- Inspect shadow DOM (enable "Show user agent shadow DOM" in settings) - View Custom Elements with their properties - Debug CSS in shadow roots
Console:- Subscribe to all PAN messages: pan.subscribe('*', console.log)
- Test components directly: document.querySelector('my-component')
- Check Custom Elements registry: customElements.get('my-component')
- Verify ES modules load correctly - Check import map resolution - Monitor API calls
Sources Panel:- Set breakpoints in your source code (no source maps needed!) - Step through component lifecycle - Watch variables and state
Your First LARC Application
Let's build something more interesting than "Hello World"—a simple counter application with multiple components communicating via the PAN bus.
Project Goal
We'll create:
- A counter display component - Increment and decrement buttons - A reset button - Communication via PAN bus (no prop drilling!)
Step 1: Update index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Counter App - LARC</title>
<script type="importmap">
{
"imports": {
"@larcjs/core": "https://cdn.jsdelivr.net/npm/@larcjs/core@3.0.1/pan.mjs"
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#app {
background: white;
padding: 40px;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
min-width: 400px;
}
</style>
</head>
<body>
<div id="app">
<counter-display></counter-display>
<counter-controls></counter-controls>
</div>
<script type="module" src="src/app.js"></script>
</body>
</html>
Step 2: Create app.js
// src/app.js
import { pan } from '@larcjs/core';
// Import components
import './components/counter-display.js';
import './components/counter-controls.js';
// Initialize application state
let count = 0;
// Listen for increment requests
pan.subscribe('counter.increment', () => {
count++;
pan.publish('counter.updated', { count });
});
// Listen for decrement requests
pan.subscribe('counter.decrement', () => {
count--;
pan.publish('counter.updated', { count });
});
// Listen for reset requests
pan.subscribe('counter.reset', () => {
count = 0;
pan.publish('counter.updated', { count });
});
// Publish initial state
pan.publish('counter.updated', { count });
console.log('Counter app initialized');
Step 3: Create counter-display.js
// src/components/counter-display.js
import { pan } from '@larcjs/core';
class CounterDisplay extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.count = 0;
}
connectedCallback() {
// Subscribe to count updates
this.unsubscribe = pan.subscribe('counter.updated', ({ count }) => {
this.count = count;
this.render();
});
this.render();
}
disconnectedCallback() {
this.unsubscribe();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
text-align: center;
margin-bottom: 30px;
}
.display {
font-size: 72px;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
font-variant-numeric: tabular-nums;
}
.label {
font-size: 18px;
color: #666;
text-transform: uppercase;
letter-spacing: 2px;
}
</style>
<div class="display">${this.count}</div>
<div class="label">Current Count</div>
`;
}
}
customElements.define('counter-display', CounterDisplay);
Step 4: Create counter-controls.js
// src/components/counter-controls.js
import { pan } from '@larcjs/core';
class CounterControls extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.attachEventListeners();
}
attachEventListeners() {
this.shadowRoot.querySelector('#increment').addEventListener('click', () => {
pan.publish('counter.increment');
});
this.shadowRoot.querySelector('#decrement').addEventListener('click', () => {
pan.publish('counter.decrement');
});
this.shadowRoot.querySelector('#reset').addEventListener('click', () => {
pan.publish('counter.reset');
});
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
button {
flex: 1;
padding: 15px;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
button:active {
transform: translateY(0);
}
#increment {
background: #48bb78;
color: white;
}
#increment:hover {
background: #38a169;
}
#decrement {
background: #f56565;
color: white;
}
#decrement:hover {
background: #e53e3e;
}
#reset {
background: #4a5568;
color: white;
width: 100%;
}
#reset:hover {
background: #2d3748;
}
</style>
<div class="controls">
<button id="decrement">− Decrement</button>
<button id="increment">+ Increment</button>
</div>
<button id="reset">Reset</button>
`;
}
}
customElements.define('counter-controls', CounterControls);
Step 5: Test Your App
Start your local server and open the page. You should see:
- A large counter display showing "0" - Increment and decrement buttons - A reset button
Click the buttons. Notice how:
- Components update immediately
- State is managed centrally in app.js
- Components don't reference each other directly
- Adding new components is trivial (just subscribe to counter.updated)
What Just Happened?
Let's examine the architecture:
Data Flow:User clicks button
↓
Controls component publishes event
↓
App.js receives event and updates state
↓
App.js publishes updated state
↓
Display component receives update and re-renders
Key Points:
app.js, not in componentsProject Structure
!Figure 3.1: LARC Deployment Architecture
Figure 3.1: LARC Deployment ArchitectureAs your application grows, organization becomes important. Here's a recommended structure:
my-app/
├── index.html # Entry point
├── larc.config.json # Optional config
├── src/
│ ├── app.js # Main application logic
│ ├── components/ # Reusable components
│ │ ├── ui/ # Generic UI components
│ │ │ ├── button.js
│ │ │ ├── card.js
│ │ │ └── modal.js
│ │ ├── features/ # Feature-specific components
│ │ │ ├── user-profile.js
│ │ │ ├── todo-list.js
│ │ │ └── dashboard.js
│ │ └── layout/ # Layout components
│ │ ├── header.js
│ │ ├── sidebar.js
│ │ └── footer.js
│ ├── lib/ # Utilities and helpers
│ │ ├── api.js # API client
│ │ ├── auth.js # Authentication
│ │ ├── router.js # Routing logic
│ │ └── utils.js # General utilities
│ ├── pages/ # Page-level components
│ │ ├── home.js
│ │ ├── dashboard.js
│ │ └── settings.js
│ └── styles/ # Global styles
│ ├── reset.css
│ ├── variables.css
│ └── utilities.css
├── public/ # Static assets
│ ├── images/
│ ├── fonts/
│ └── icons/
└── tests/ # Test files
├── components/
└── integration/
File Organization Principles
Components:- One component per file
- File name matches component name: user-profile.js defines
- Keep related components together in subdirectories
- Utilities that don't render UI - API clients, helpers, formatters - Pure functions when possible
Pages:- Top-level route components - Compose smaller components - Handle page-specific logic
Styles:- Global styles in styles/
- Component-specific styles in Shadow DOM
- CSS custom properties for theming
Import Maps Explained
!Figure 3.2: Module Loading with Import Maps
Figure 3.2: Module Loading with Import MapsImport Maps are a browser standard that replaces the need for bundlers to resolve module paths.
Basic Import Map
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4/lodash.js",
"dayjs": "https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"
}
}
</script>
<script type="module">
// Use package names instead of URLs
import _ from 'lodash';
import dayjs from 'dayjs';
console.log(dayjs().format('YYYY-MM-DD'));
</script>
Path Aliases
Create shortcuts for your own modules:
<script type="importmap">
{
"imports": {
"@/": "/src/",
"components/": "/",
"lib/": "/src/lib/",
"@larcjs/core": "https://cdn.jsdelivr.net/npm/@larcjs/core@3.0.1/pan.mjs"
}
}
</script>
<script type="module">
// Instead of: import { api } from '../../../lib/api.js';
import { api } from 'lib/api.js';
// Instead of: import Button from '../components/ui/button.js';
import Button from 'components/ui/button.js';
// Instead of: import something from '../../../src/utils.js';
import something from '@/utils.js';
</script>
Version Management
Pin dependencies to specific versions:
{
"imports": {
"@larcjs/core": "https://cdn.jsdelivr.net/npm/@larcjs/core@3.0.1/dist/index.js",
"@larcjs/ui": "https://cdn.jsdelivr.net/npm/@larcjs/ui@3.0.1/dist/index.js"
}
}
Or use version ranges for automatic updates:
{
"imports": {
"@larcjs/core": "https://cdn.jsdelivr.net/npm/@larcjs/core@3.0.1/pan.mjs",
"@larcjs/ui": "https://cdn.jsdelivr.net/npm/@larcjs/ui@3.0.1/dist/index.js"
}
}
Multiple CDNs
Add fallbacks for reliability:
{
"imports": {
"react": "https://esm.sh/react@18",
"react-fallback": "https://cdn.skypack.dev/react@18"
}
}
Then in code:
let React;
try {
React = await import('react');
} catch {
React = await import('react-fallback');
}
Development vs Production
Use different import maps for different environments:
development.importmap.json:{
"imports": {
"@larcjs/core": "/node_modules/@larcjs/core/dist/index.js",
"app/": "/src/"
}
}
production.importmap.json:
{
"imports": {
"@larcjs/core": "https://cdn.jsdelivr.net/npm/@larcjs/core@3.0.1/dist/index.js",
"app/": "/assets/js/"
}
}
Load the appropriate map:
<script type="importmap" src="/config/production.importmap.json"></script>
Development Workflow
Daily Development
A typical development session:
1. Start dev server:larc dev
This starts a local server with hot reload.
2. Edit files: Open your editor and make changes. The browser automatically reloads when you save. 3. Check the console: Open browser DevTools and check for errors or warnings. 4. Test in browser: Interact with your app, verify behavior, check responsive design. 5. Debug as needed: Set breakpoints, inspect elements, monitor network requests. 6. Repeat: The edit-refresh cycle is instant with no build step.Debugging Tips
Log all PAN messages:pan.subscribe('*', (topic, data) => {
console.log(`[PAN] ${topic}:`, data);
});
Inspect custom elements:
// Get element
const el = document.querySelector('my-component');
// Check if defined
console.log(customElements.get('my-component'));
// Access shadow root
console.log(el.shadowRoot);
// Call methods directly
el.someMethod();
Monitor attribute changes:
// Create observer
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('Attribute changed:', mutation.attributeName);
});
});
// Watch element
observer.observe(element, { attributes: true });
Testing
Run tests without a build step:
<!-- tests/counter.test.html -->
<!DOCTYPE html>
<html>
<head>
<title>Counter Tests</title>
<script type="importmap">
{
"imports": {
"@larcjs/core": "../node_modules/@larcjs/core/dist/index.js"
}
}
</script>
</head>
<body>
<div id="test-container"></div>
<script type="module">
import { pan } from '@larcjs/core';
import '../counter-display.js';
// Simple test framework
function test(name, fn) {
try {
fn();
console.log(`✓ ${name}`);
} catch (error) {
console.error(`✗ ${name}:`, error);
}
}
function assert(condition, message) {
if (!condition) throw new Error(message || 'Assertion failed');
}
// Tests
test('counter-display renders initial count', () => {
const el = document.createElement('counter-display');
document.getElementById('test-container').appendChild(el);
const display = el.shadowRoot.querySelector('.display');
assert(display.textContent === '0', 'Initial count should be 0');
el.remove();
});
test('counter-display updates on PAN message', async () => {
const el = document.createElement('counter-display');
document.getElementById('test-container').appendChild(el);
// Wait for component to connect
await new Promise(resolve => setTimeout(resolve, 10));
// Publish update
pan.publish('counter.updated', { count: 42 });
// Wait for render
await new Promise(resolve => setTimeout(resolve, 10));
const display = el.shadowRoot.querySelector('.display');
assert(display.textContent === '42', 'Count should update to 42');
el.remove();
});
console.log('All tests complete');
</script>
</body>
</html>
Open tests/counter.test.html in your browser to run tests.
Common Patterns
Pattern 1: Loading States
class DataComponent extends HTMLElement {
async connectedCallback() {
this.render({ loading: true });
try {
const data = await this.fetchData();
this.render({ data });
} catch (error) {
this.render({ error: error.message });
}
}
render(state) {
if (state.loading) {
this.innerHTML = '<loading-spinner></loading-spinner>';
} else if (state.error) {
this.innerHTML = `<error-message>${state.error}</error-message>`;
} else {
this.innerHTML = `<data-display .data="${state.data}"></data-display>`;
}
}
}
Pattern 2: Form Handling
class LoginForm extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<form>
<input type="email" name="email" required>
<input type="password" name="password" required>
<button type="submit">Login</button>
</form>
`;
this.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
pan.publish('auth.login', data);
});
}
}
Pattern 3: Conditional Rendering
class UserMenu extends HTMLElement {
constructor() {
super();
this.user = null;
}
connectedCallback() {
pan.subscribe('auth.user.changed', ({ user }) => {
this.user = user;
this.render();
});
this.render();
}
render() {
if (this.user) {
this.innerHTML = `
<div class="logged-in">
<span>Hello, ${this.user.name}</span>
<button id="logout">Logout</button>
</div>
`;
this.querySelector('#logout').addEventListener('click', () => {
pan.publish('auth.logout');
});
} else {
this.innerHTML = `
<button id="login">Login</button>
`;
this.querySelector('#login').addEventListener('click', () => {
pan.publish('app.navigate', { path: '/login' });
});
}
}
}
Pattern 4: Lists and Iteration
class TodoList extends HTMLElement {
constructor() {
super();
this.todos = [];
}
connectedCallback() {
pan.subscribe('todos.updated', ({ todos }) => {
this.todos = todos;
this.render();
});
this.render();
}
render() {
this.innerHTML = `
<ul>
${this.todos.map(todo => `
<li>
<input type="checkbox"
${todo.completed ? 'checked' : ''}
data-id="${todo.id}">
<span class="${todo.completed ? 'completed' : ''}">
${todo.text}
</span>
</li>
`).join('')}
</ul>
`;
// Attach event listeners after rendering
this.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const id = e.target.dataset.id;
pan.publish('todos.toggle', { id });
});
});
}
}
Summary
In this chapter, you:
- Set up a LARC development environment (CLI or manual) - Built your first multi-component application - Learned project structure best practices - Mastered Import Maps for dependency management - Established an efficient development workflow - Explored common component patterns
You now have a solid foundation for building LARC applications. The next chapter dives deeper into creating sophisticated Web Components with proper lifecycle management, styling, and interactivity.
Exercises
1. Enhance the Counter App:- Add a history component that shows past values - Add increment/decrement by custom amounts - Persist count to localStorage
2. Build a Todo List:- Add/remove todos - Mark as complete/incomplete - Filter by status (all/active/completed) - Use PAN bus for state management
3. Create a Theme Switcher:- Light/dark theme toggle - Publish theme changes via PAN - Multiple components respond to theme changes - Persist theme preference
4. Experiment with Import Maps:- Try different CDNs (jsDelivr, unpkg, esm.sh) - Add path aliases for your components - Import an external library (lodash, dayjs, etc.)
Take your time with these exercises. Understanding these patterns now will make the rest of the book much easier.
Further Reading
For detailed API reference and configuration options:- Building with LARC Chapter 3: Getting Started - Complete installation options and troubleshooting
- Building with LARC Chapter 2: Core Concepts - Architecture and messaging patterns reference
- Building with LARC Appendix C: Configuration Options - All configuration parameters