lomer-ui is a dead-simple CLI tool that instantly kickstarts your Svelte components, allowing developers to rapidly scaffold production-ready component structures with best practices built-in. This article covers advanced component scaffolding, custom template creation, and integration with SvelteKit workflows for enterprise-level development. This is part 1 of a series on using lomer-ui with Svelte.
This guide walks through creating a sophisticated component scaffolding system using lomer-ui CLI with Svelte, from initial setup to building custom templates and automating component generation workflows.
Prerequisites
Before starting, ensure you have:
- A SvelteKit project (SvelteKit 1.0+ or standalone Svelte 4+)
- Node.js 18+ and npm/pnpm/yarn
- Basic understanding of Svelte component architecture, props, slots, and lifecycle
- Familiarity with CLI tools and command-line interfaces
- Understanding of project structure and component organization patterns
Action example: The lomer generate command creates a new component with a predefined structure. When you run lomer generate Button, it scaffolds a complete component file with TypeScript types, proper prop definitions, and a basic template structure. The CLI analyzes your project structure and automatically places components in the correct directory based on your configuration.
Installation
Install lomer-ui globally or as a dev dependency:
# Global installation (recommended for CLI tools)
npm install -g lomer-ui
# Or as a dev dependency
npm install -D lomer-ui
Verify installation:
lomer --version
Project Setup
Initialize lomer-ui in your SvelteKit project:
lomer init
This creates a lomer.config.js configuration file in your project root:
// lomer.config.js
export default {
// Component output directory
componentsDir: 'src/lib/components',
// Template directory for custom templates
templatesDir: 'lomer-templates',
// Default component structure
defaultStructure: {
includeTypes: true,
includeTests: false,
includeStories: false,
includeStyles: true
},
// File naming conventions
naming: {
component: 'PascalCase',
file: 'kebab-case'
},
// SvelteKit specific settings
sveltekit: {
useLib: true,
generateIndex: true
}
}
Create the components directory structure:
mkdir -p src/lib/components
mkdir -p lomer-templates
First Example / Basic Usage
Let's create your first component using lomer-ui:
lomer generate Button
This generates a complete component structure:
<!-- src/lib/components/button/Button.svelte -->
<script lang="ts">
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
}
let {
variant = 'primary',
size = 'md',
disabled = false,
type = 'button',
...restProps
}: ButtonProps = $props();
// Component logic here
function handleClick(event: MouseEvent) {
if (disabled) return;
// Handle click logic
}
</script>
<button
class="btn btn-{variant} btn-{size}"
{disabled}
{type}
onclick={handleClick}
{...restProps}
>
<slot />
</button>
<style>
.btn {
@apply px-4 py-2 rounded font-medium transition-colors;
}
.btn-primary {
@apply bg-blue-600 text-white hover:bg-blue-700;
}
.btn-secondary {
@apply bg-gray-600 text-white hover:bg-gray-700;
}
.btn-outline {
@apply border-2 border-blue-600 text-blue-600 hover:bg-blue-50;
}
.btn-sm {
@apply px-2 py-1 text-sm;
}
.btn-md {
@apply px-4 py-2;
}
.btn-lg {
@apply px-6 py-3 text-lg;
}
.btn:disabled {
@apply opacity-50 cursor-not-allowed;
}
</style>
The CLI also generates a TypeScript types file:
// src/lib/components/button/Button.types.ts
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
}
export type ButtonVariant = ButtonProps['variant'];
export type ButtonSize = ButtonProps['size'];
And an index file for easy imports:
// src/lib/components/button/index.ts
export { default as Button } from './Button.svelte';
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button.types';
Understanding the Basics
lomer-ui integrates seamlessly with Svelte's component architecture. The CLI understands Svelte's runes system (introduced in Svelte 5) and generates components using modern patterns:
- Props with $props(): Uses Svelte 5's new props syntax for better type safety
-
Reactive statements: Automatically includes
$state()and$derived()when needed - Slot support: Generates proper slot structures for composable components
- TypeScript integration: Full type safety with proper interfaces and exports
The generated components follow SvelteKit conventions, placing reusable components in src/lib/components for easy import across your application.
Practical Example / Building Something Real
Let's build a complete, production-ready DataTable component with advanced features. This demonstrates how lomer-ui can scaffold complex components with proper state management, accessibility, and error handling.
Step 1: Generate the Base Component
lomer generate DataTable --with-types --with-styles
Step 2: Implement the Complete Component
<!-- src/lib/components/data-table/DataTable.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import type { DataTableProps, TableColumn, TableData } from './DataTable.types';
interface DataTableProps {
columns: TableColumn[];
data: TableData[];
loading?: boolean;
sortable?: boolean;
filterable?: boolean;
pageSize?: number;
onRowClick?: (row: TableData) => void;
}
let {
columns,
data = [],
loading = false,
sortable = true,
filterable = true,
pageSize = 10,
onRowClick
}: DataTableProps = $props();
// State management
let currentPage = $state(1);
let sortColumn = $state<string | null>(null);
let sortDirection = $state<'asc' | 'desc'>('asc');
let filterValue = $state('');
let filteredData = $derived.by(() => {
let result = [...data];
// Apply filtering
if (filterValue && filterable) {
const searchLower = filterValue.toLowerCase();
result = result.filter(row =>
columns.some(col => {
const value = row[col.key];
return value?.toString().toLowerCase().includes(searchLower);
})
);
}
// Apply sorting
if (sortColumn && sortable) {
result.sort((a, b) => {
const aVal = a[sortColumn];
const bVal = b[sortColumn];
const modifier = sortDirection === 'asc' ? 1 : -1;
if (aVal < bVal) return -1 * modifier;
if (aVal > bVal) return 1 * modifier;
return 0;
});
}
return result;
});
let paginatedData = $derived.by(() => {
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
return filteredData.slice(start, end);
});
let totalPages = $derived.by(() =>
Math.ceil(filteredData.length / pageSize)
);
// Event handlers with error handling
function handleSort(columnKey: string) {
if (!sortable) return;
try {
if (sortColumn === columnKey) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortColumn = columnKey;
sortDirection = 'asc';
}
currentPage = 1; // Reset to first page on sort
} catch (error) {
console.error('Error sorting table:', error);
}
}
function handleRowClick(row: TableData) {
if (onRowClick) {
try {
onRowClick(row);
} catch (error) {
console.error('Error handling row click:', error);
}
}
}
function handlePageChange(newPage: number) {
if (newPage >= 1 && newPage <= totalPages) {
currentPage = newPage;
// Scroll to top of table for better UX
const tableElement = document.querySelector('[data-table-container]');
tableElement?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
// Accessibility: Keyboard navigation
function handleKeydown(event: KeyboardEvent, row: TableData) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleRowClick(row);
}
}
</script>
<div class="data-table-container" data-table-container>
<!-- Filter input -->
{#if filterable}
<div class="filter-section" role="search">
<label for="table-filter" class="sr-only">Filter table</label>
<input
id="table-filter"
type="text"
placeholder="Search..."
bind:value={filterValue}
class="filter-input"
aria-label="Filter table data"
/>
</div>
{/if}
<!-- Loading state -->
{#if loading}
<div class="loading-state" role="status" aria-live="polite">
<span class="spinner" aria-hidden="true"></span>
<span>Loading data...</span>
</div>
{:else}
<!-- Table -->
<div class="table-wrapper">
<table class="data-table" role="table" aria-label="Data table">
<thead>
<tr>
{#each columns as column}
<th
class="table-header"
class:sortable={sortable}
class:sorted={sortColumn === column.key}
class:sort-asc={sortColumn === column.key && sortDirection === 'asc'}
class:sort-desc={sortColumn === column.key && sortDirection === 'desc'}
onclick={() => handleSort(column.key)}
role="columnheader"
aria-sort={sortColumn === column.key
? (sortDirection === 'asc' ? 'ascending' : 'descending')
: 'none'}
>
{column.label}
{#if sortable && sortColumn === column.key}
<span class="sort-indicator" aria-hidden="true">
{sortDirection === 'asc' ? '↑' : '↓'}
</span>
{/if}
</th>
{/each}
</tr>
</thead>
<tbody>
{#if paginatedData.length === 0}
<tr>
<td colspan={columns.length} class="empty-state">
No data available
</td>
</tr>
{:else}
{#each paginatedData as row (row.id || row)}
<tr
class="table-row"
class:clickable={!!onRowClick}
onclick={() => handleRowClick(row)}
onkeydown={(e) => handleKeydown(e, row)}
tabindex={onRowClick ? 0 : -1}
role="row"
>
{#each columns as column}
<td class="table-cell" role="cell">
{#if column.render}
{@html column.render(row[column.key], row)}
{:else}
{row[column.key] ?? '—'}
{/if}
</td>
{/each}
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
<!-- Pagination -->
{#if totalPages > 1}
<nav class="pagination" role="navigation" aria-label="Table pagination">
<button
class="pagination-btn"
disabled={currentPage === 1}
onclick={() => handlePageChange(currentPage - 1)}
aria-label="Previous page"
>
Previous
</button>
<span class="pagination-info" aria-live="polite">
Page {currentPage} of {totalPages}
</span>
<button
class="pagination-btn"
disabled={currentPage === totalPages}
onclick={() => handlePageChange(currentPage + 1)}
aria-label="Next page"
>
Next
</button>
</nav>
{/if}
{/if}
</div>
<style>
.data-table-container {
@apply w-full;
}
.filter-section {
@apply mb-4;
}
.filter-input {
@apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500;
}
.loading-state {
@apply flex items-center justify-center py-12 text-gray-500;
}
.spinner {
@apply inline-block w-4 h-4 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin mr-2;
}
.table-wrapper {
@apply overflow-x-auto;
}
.data-table {
@apply w-full border-collapse;
}
.table-header {
@apply px-4 py-3 text-left bg-gray-50 font-semibold text-gray-700 border-b border-gray-200;
}
.table-header.sortable {
@apply cursor-pointer hover:bg-gray-100 transition-colors;
}
.table-header.sorted {
@apply bg-blue-50 text-blue-700;
}
.sort-indicator {
@apply ml-2 text-blue-600;
}
.table-row {
@apply border-b border-gray-200 hover:bg-gray-50 transition-colors;
}
.table-row.clickable {
@apply cursor-pointer;
}
.table-row:focus {
@apply outline-none ring-2 ring-blue-500 ring-offset-2;
}
.table-cell {
@apply px-4 py-3 text-gray-700;
}
.empty-state {
@apply text-center py-8 text-gray-500;
}
.pagination {
@apply flex items-center justify-between mt-4;
}
.pagination-btn {
@apply px-4 py-2 border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors;
}
.pagination-info {
@apply text-sm text-gray-600;
}
.sr-only {
@apply sr-only;
}
</style>
Step 3: Type Definitions
// src/lib/components/data-table/DataTable.types.ts
export interface TableColumn {
key: string;
label: string;
render?: (value: any, row: TableData) => string;
sortable?: boolean;
}
export interface TableData {
[key: string]: any;
id?: string | number;
}
export interface DataTableProps {
columns: TableColumn[];
data: TableData[];
loading?: boolean;
sortable?: boolean;
filterable?: boolean;
pageSize?: number;
onRowClick?: (row: TableData) => void;
}
Step 4: Usage Example
<!-- src/routes/users/+page.svelte -->
<script lang="ts">
import DataTable from '$lib/components/data-table/DataTable.svelte';
import type { TableColumn, TableData } from '$lib/components/data-table/DataTable.types';
let users = $state<TableData[]>([]);
let loading = $state(true);
const columns: TableColumn[] = [
{ key: 'id', label: 'ID', sortable: true },
{ key: 'name', label: 'Name', sortable: true },
{ key: 'email', label: 'Email', sortable: true },
{
key: 'role',
label: 'Role',
render: (value) => `<span class="badge badge-${value}">${value}</span>`
},
{ key: 'createdAt', label: 'Created', sortable: true }
];
async function loadUsers() {
try {
loading = true;
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to load users');
users = await response.json();
} catch (error) {
console.error('Error loading users:', error);
} finally {
loading = false;
}
}
function handleRowClick(user: TableData) {
// Navigate to user detail page
window.location.href = `/users/${user.id}`;
}
onMount(() => {
loadUsers();
});
</script>
<div class="users-page">
<h1>Users Management</h1>
<DataTable
{columns}
data={users}
{loading}
sortable={true}
filterable={true}
pageSize={10}
onRowClick={handleRowClick}
/>
</div>
This example demonstrates:
- Real-world context: A production-ready data table for user management
- State management: Proper use of Svelte 5 runes ($state, $derived)
- User interaction: Sorting, filtering, pagination, and row clicks
- Edge cases: Empty states, loading states, error handling
- Accessibility: ARIA labels, keyboard navigation, semantic HTML
- Performance: Derived state for efficient filtering and sorting
Common Issues / Troubleshooting
Issue 1: Components not found after generation
Error message: Cannot find module '$lib/components/Button'
Solution: Ensure your lomer.config.js matches your SvelteKit alias configuration. Check vite.config.js or svelte.config.js:
// vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
export default {
plugins: [sveltekit()],
resolve: {
alias: {
$lib: path.resolve('./src/lib')
}
}
}
Prevention: Run lomer init after setting up your SvelteKit project to auto-detect paths.
Issue 2: TypeScript errors in generated components
Error message: Property 'variant' does not exist on type 'ComponentProps<Button>'
Solution: Ensure you're using Svelte 5 with runes enabled. Update your component to use $props():
<script lang="ts">
// Svelte 5 syntax
let { variant = 'primary' }: { variant?: string } = $props();
</script>
Debugging tip: Check your package.json for Svelte version. lomer-ui requires Svelte 5+ for full TypeScript support.
Issue 3: Generated components don't match project style
Solution: Create custom templates. See the "Custom Templates" section below.
Prevention: Configure lomer.config.js with your preferred structure before generating components.
Issue 4: CLI command not found
Error message: 'lomer' is not recognized as an internal or external command
Solution:
- For global install: Ensure npm global bin is in your PATH
- For local install: Use
npx lomeror add to package.json scripts:
{
"scripts": {
"generate:component": "lomer generate"
}
}
Version conflicts: If using different Node versions, consider using nvm or ensure consistent Node.js version across team.
Next Steps
- Custom Templates: Learn to create reusable component templates for your team's specific patterns
- Component Library: Build a complete design system with lomer-ui scaffolding
- Integration: Explore integrating lomer-ui with Storybook for component documentation
- Automation: Set up pre-commit hooks to enforce component structure standards
Practice suggestions:
- Create a set of 5-10 commonly used components (Button, Input, Modal, Card, etc.)
- Build a component library documentation site
- Implement a design system with consistent styling patterns
Related libraries:
-
@storybook/svelte- Component documentation and testing -
svelte-check- TypeScript checking for Svelte projects -
prettier-plugin-svelte- Code formatting for generated components
Summary
This guide covered setting up lomer-ui CLI for advanced component scaffolding in Svelte projects. You learned how to generate production-ready components with TypeScript, proper state management, accessibility features, and error handling. You should now be able to scaffold complex components like the DataTable example and customize the generation process to match your project's needs.
Top comments (0)