Skip to content

Layout Components

Qelos provides flexible layout components to help you structure your application pages and create responsive designs.

📄 page-container

Main layout wrapper for pages with header, sidebar, and content areas.

Basic Layout

vue
<script setup>
import { ref } from 'vue'

const pageTitle = ref('Dashboard')
const breadcrumb = ref([
  { text: 'Home', to: '/' },
  { text: 'Dashboard' }
])

const handleCreate = () => {
  console.log('Create clicked')
}
</script>

<template>
  <page-container>
    <!-- Page header -->
    <template #header>
      <page-title :title="pageTitle" :breadcrumb="breadcrumb">
        <template #actions>
          <el-button @click="handleCreate" type="primary">
            Create New
          </el-button>
        </template>
      </page-title>
    </template>
    
    <!-- Main content -->
    <div class="dashboard-content">
      <h2>Welcome to Dashboard</h2>
      <p>Your main content goes here.</p>
    </div>
  </page-container>
</template>

With Sidebar

vue
<script setup>
import { ref } from 'vue'

const filters = ref({
  status: '',
  dateRange: [],
  category: ''
})

const statistics = ref([
  { label: 'Total Users', value: 1234, trend: '+12%' },
  { label: 'Revenue', value: '$45,678', trend: '+23%' },
  { label: 'Orders', value: 567, trend: '-5%' }
])

const applyFilters = () => {
  console.log('Applying filters:', filters.value)
}

const resetFilters = () => {
  filters.value = {
    status: '',
    dateRange: [],
    category: ''
  }
}
</script>

<template>
  <page-container>
    <!-- Header -->
    <template #header>
      <page-title title="Analytics">
        <template #subtitle>
          View your business metrics and performance
        </template>
      </page-title>
    </template>
    
    <!-- Sidebar -->
    <template #sidebar>
      <data-card title="Filters" size="small">
        <el-form label-position="top">
          <el-form-item label="Status">
            <el-select v-model="filters.status" placeholder="All statuses">
              <el-option label="Active" value="active" />
              <el-option label="Inactive" value="inactive" />
            </el-select>
          </el-form-item>
          
          <el-form-item label="Date Range">
            <el-date-picker
              v-model="filters.dateRange"
              type="daterange"
              placeholder="Select range"
              style="width: 100%"
            />
          </el-form-item>
          
          <el-form-item label="Category">
            <el-select v-model="filters.category" placeholder="All categories">
              <el-option label="Sales" value="sales" />
              <el-option label="Marketing" value="marketing" />
              <el-option label="Support" value="support" />
            </el-select>
          </el-form-item>
          
          <el-form-item>
            <el-button type="primary" @click="applyFilters" style="width: 100%">
              Apply Filters
            </el-button>
            <el-button @click="resetFilters" style="width: 100%; margin-top: 8px">
              Reset
            </el-button>
          </el-form-item>
        </el-form>
      </data-card>
    </template>
    
    <!-- Main content -->
    <div class="analytics-content">
      <!-- Statistics cards -->
      <grid-layout :cols="{ xs: 1, sm: 2, lg: 3 }" gap="20">
        <stat-card
          v-for="stat in statistics"
          :key="stat.label"
          :title="stat.label"
          :value="stat.value"
          :trend="stat.trend"
        />
      </grid-layout>
      
      <!-- Charts and tables -->
      <data-card title="Performance Chart" style="margin-top: 20px">
        <!-- Chart content -->
        <div class="chart-placeholder">
          Chart will be rendered here
        </div>
      </data-card>
    </div>
  </page-container>
</template>

<style scoped>
.analytics-content {
  padding: 20px 0;
}

.chart-placeholder {
  height: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--qelos-surface-hover);
  border-radius: 4px;
  color: var(--qelos-text-secondary);
}
</style>
vue
<script setup>
import { ref } from 'vue'

const loading = ref(false)
const data = ref([])

const loadData = async () => {
  loading.value = true
  try {
    // Fetch data
    await new Promise(resolve => setTimeout(resolve, 1000))
    data.value = Array(50).fill(0).map((_, i) => ({
      id: i + 1,
      name: `Item ${i + 1}`
    }))
  } finally {
    loading.value = false
  }
}

const exportData = () => {
  console.log('Exporting data:', data.value)
}

const refreshData = () => {
  loadData()
}
</script>

<template>
  <page-container>
    <template #header>
      <page-title title="Data Management" />
    </template>
    
    <template #footer>
      <div class="page-footer">
        <div class="footer-info">
          <span>Total: {{ data.length }} items</span>
          <span v-if="loading">Loading...</span>
        </div>
        <div class="footer-actions">
          <el-button @click="exportData" :disabled="!data.length">
            Export
          </el-button>
          <el-button @click="refreshData" :loading="loading">
            Refresh
          </el-button>
        </div>
      </div>
    </template>
    
    <div class="data-content">
      <quick-table
        :data="data"
        :loading="loading"
        :columns="[
          { key: 'id', label: 'ID' },
          { key: 'name', label: 'Name' }
        ]"
      />
    </div>
  </page-container>
</template>

<style scoped>
.page-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 0;
  border-top: 1px solid var(--qelos-border);
  margin-top: 20px;
}

.footer-info {
  display: flex;
  gap: 20px;
  color: var(--qelos-text-secondary);
}

.footer-actions {
  display: flex;
  gap: 10px;
}
</style>

Props

PropTypeDefaultDescription
paddingstring/number-Container padding
max-widthstring/number-Maximum width
fluidbooleanfalseFull width container
centeredbooleanfalseCenter content

Slots

SlotDescription
defaultMain content area
headerPage header
sidebarLeft sidebar
footerPage footer

📐 grid-layout

Responsive grid system with auto-fit and masonry support.

Basic Grid

vue
<script setup>
import { ref } from 'vue'

const items = ref(Array(12).fill(0).map((_, i) => ({
  id: i + 1,
  title: `Item ${i + 1}`,
  content: `Content for item ${i + 1}`
}))

const cols = {
  xs: 1,
  sm: 2,
  md: 3,
  lg: 4,
  xl: 6
}
</script>

<template>
  <div class="grid-demo">
    <h3>Responsive Grid</h3>
    <grid-layout :cols="cols" gap="20">
      <data-card
        v-for="item in items"
        :key="item.id"
        :title="item.title"
        size="small"
      >
        {{ item.content }}
      </data-card>
    </grid-layout>
  </div>
</template>

Custom Column Spans

vue
<script setup>
import { ref } from 'vue'

const layoutSections = ref([
  { span: 8, content: 'Main content (8/12)' },
  { span: 4, content: 'Sidebar (4/12)' },
  { span: 12, content: 'Full width (12/12)' },
  { span: 6, content: 'Half width (6/12)' },
  { span: 6, content: 'Half width (6/12)' }
])
</script>

<template>
  <div class="custom-grid">
    <grid-layout :cols="12" gap="20">
      <div
        v-for="(section, index) in layoutSections"
        :key="index"
        class="grid-section"
        :span="section.span"
      >
        <data-card :title="`Span ${section.span}`">
          {{ section.content }}
        </data-card>
      </div>
    </grid-layout>
  </div>
</template>

<style scoped>
.grid-section {
  min-height: 100px;
}
</style>

Masonry Layout

vue
<script setup>
import { ref } from 'vue'

const masonryItems = ref([
  { id: 1, height: 200, color: '#409eff' },
  { id: 2, height: 150, color: '#67c23a' },
  { id: 3, height: 250, color: '#e6a23c' },
  { id: 4, height: 180, color: '#f56c6c' },
  { id: 5, height: 220, color: '#909399' },
  { id: 6, height: 160, color: '#409eff' },
  { id: 7, height: 190, color: '#67c23a' },
  { id: 8, height: 210, color: '#e6a23c' }
])
</script>

<template>
  <div class="masonry-demo">
    <h3>Masonry Layout</h3>
    <grid-layout :cols="{ xs: 1, sm: 2, lg: 3, xl: 4 }" gap="15" masonry>
      <div
        v-for="item in masonryItems"
        :key="item.id"
        class="masonry-item"
        :style="{ height: item.height + 'px', background: item.color }"
      >
        <div class="item-content">
          <h4>Item {{ item.id }}</h4>
          <p>Height: {{ item.height }}px</p>
        </div>
      </div>
    </grid-layout>
  </div>
</template>

<style scoped>
.masonry-demo h3 {
  margin-bottom: 20px;
}

.masonry-item {
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 500;
}

.item-content {
  text-align: center;
}

.item-content h4 {
  margin: 0 0 5px 0;
}

.item-content p {
  margin: 0;
  opacity: 0.9;
}
</style>

Auto-Fit Grid

vue
<script setup>
import { ref } from 'vue'

const products = ref(Array(20).fill(0).map((_, i) => ({
  id: i + 1,
  name: `Product ${i + 1}`,
  price: Math.floor(Math.random() * 100) + 10
})))
</script>

<template>
  <div class="auto-fit-grid">
    <h3>Auto-Fit Grid</h3>
    <p>Cards automatically adjust to available space</p>
    
    <grid-layout :cols="{ min: '250px', max: '1fr' }" gap="20">
      <data-card
        v-for="product in products"
        :key="product.id"
        :title="product.name"
        size="small"
      >
        <div class="product-info">
          <p class="price">${{ product.price }}</p>
          <el-button size="small" type="primary">View Details</el-button>
        </div>
      </data-card>
    </grid-layout>
  </div>
</template>

<style scoped>
.auto-fit-grid p {
  color: var(--qelos-text-secondary);
  margin-bottom: 20px;
}

.product-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
}

.price {
  font-weight: 600;
  color: var(--qelos-primary);
}
</style>

Props

PropTypeDefaultDescription
colsnumber/Object12Column count or responsive config
gapstring/number0Gap between items
masonrybooleanfalseEnable masonry layout
auto-fitbooleanfalseAuto-fit columns

Responsive Columns

javascript
// Responsive column configuration
const cols = {
  xs: 1,    // Mobile (< 576px)
  sm: 2,    // Small tablets (≥ 576px)
  md: 3,    // Tablets (≥ 768px)
  lg: 4,    // Desktop (≥ 992px)
  xl: 5,    // Large desktop (≥ 1200px)
  xxl: 6    // Extra large (≥ 1400px)
}

// Auto-fit configuration
const autoFitCols = {
  min: '300px',  // Minimum column width
  max: '1fr'     // Maximum column width
}

📑 page-section

Section component for organizing page content with optional background and spacing.

Basic Sections

vue
<script setup>
import { ref } from 'vue'

const sections = ref([
  {
    title: 'Introduction',
    content: 'This is the introduction section with important information.',
    background: 'default'
  },
  {
    title: 'Features',
    content: 'Here we highlight the key features of our product.',
    background: 'light'
  },
  {
    title: 'Get Started',
    content: 'Ready to begin? Follow these simple steps.',
    background: 'primary',
    dark: true
  }
])
</script>

<template>
  <div class="page-sections">
    <page-section
      v-for="(section, index) in sections"
      :key="index"
      :title="section.title"
      :background="section.background"
      :dark="section.dark"
      :padding="index === 0 ? 'large' : 'medium'"
    >
      <p>{{ section.content }}</p>
      
      <template v-if="index === 2" #actions>
        <el-button size="large" type="primary">
          Get Started Now
        </el-button>
      </template>
    </page-section>
  </div>
</template>

Feature Grid Section

vue
<script setup>
import { ref } from 'vue'

const features = ref([
  {
    icon: 'rocket',
    title: 'Fast Performance',
    description: 'Lightning fast loading times and smooth interactions.'
  },
  {
    icon: 'shield',
    title: 'Secure',
    description: 'Enterprise-grade security to protect your data.'
  },
  {
    icon: 'mobile',
    title: 'Mobile Ready',
    description: 'Responsive design that works on all devices.'
  },
  {
    icon: 'chart',
    title: 'Analytics',
    description: 'Detailed insights and analytics for your data.'
  },
  {
    icon: 'users',
    title: 'Team Collaboration',
    description: 'Work together seamlessly with your team.'
  },
  {
    icon: 'settings',
    title: 'Customizable',
    description: 'Tailor the experience to your specific needs.'
  }
])
</script>

<template>
  <page-section
    title="Powerful Features"
    subtitle="Everything you need to succeed"
    background="light"
    padding="large"
  >
    <grid-layout :cols="{ xs: 1, sm: 2, lg: 3 }" gap="30">
      <div v-for="feature in features" :key="feature.title" class="feature-card">
        <div class="feature-icon">
          <el-icon :size="48"><component :is="feature.icon" /></el-icon>
        </div>
        <h3>{{ feature.title }}</h3>
        <p>{{ feature.description }}</p>
      </div>
    </grid-layout>
  </page-section>
</template>

<style scoped>
.feature-card {
  text-align: center;
  padding: 30px 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  transition: transform 0.3s;
}

.feature-card:hover {
  transform: translateY(-5px);
}

.feature-icon {
  color: var(--qelos-primary);
  margin-bottom: 20px;
}

.feature-card h3 {
  margin-bottom: 10px;
  color: var(--qelos-text-primary);
}

.feature-card p {
  color: var(--qelos-text-secondary);
  line-height: 1.6;
}
</style>

Props

PropTypeDefaultDescription
titlestring-Section title
subtitlestring-Section subtitle
backgroundstring'default'Background: default, light, dark, primary
darkbooleanfalseDark text mode
paddingstring'medium'Padding: none, small, medium, large
full-widthbooleanfalseFull width section
containerbooleantrueWrap in container

Slots

SlotDescription
defaultSection content
actionsAction buttons area
backgroundCustom background content

📱 responsive-container

Container that provides responsive breakpoints for conditional rendering.

Responsive Content

vue
<script setup>
import { ref } from 'vue'

const screenSize = ref('unknown')

const handleScreenChange = (size) => {
  screenSize.value = size
  console.log('Screen size changed to:', size)
}
</script>

<template>
  <responsive-container @change="handleScreenChange">
    <template #default="{ size, isMobile, isTablet, isDesktop }">
      <div class="responsive-demo">
        <p>Current screen: {{ size }}</p>
        <p>Is mobile: {{ isMobile }}</p>
        <p>Is tablet: {{ isTablet }}</p>
        <p>Is desktop: {{ isDesktop }}</p>
        
        <!-- Mobile only content -->
        <div v-if="isMobile" class="mobile-content">
          <el-button type="primary" block>
            Mobile Action
          </el-button>
        </div>
        
        <!-- Desktop content -->
        <div v-else-if="isDesktop" class="desktop-content">
          <el-button-group>
            <el-button>Action 1</el-button>
            <el-button>Action 2</el-button>
            <el-button>Action 3</el-button>
          </el-button-group>
        </div>
        
        <!-- Tablet content -->
        <div v-else class="tablet-content">
          <el-button>Action</el-button>
        </div>
      </div>
    </template>
  </responsive-container>
</template>

<style scoped>
.responsive-demo {
  padding: 20px;
  text-align: center;
}

.mobile-content,
.tablet-content,
.desktop-content {
  margin-top: 20px;
  padding: 20px;
  background: var(--qelos-surface);
  border-radius: 4px;
}
</style>

Breakpoint-Specific Layouts

vue
<template>
  <responsive-container>
    <template #xs="{ size }">
      <!-- Mobile layout -->
      <grid-layout :cols="1" gap="10">
        <data-card v-for="i in 4" :key="i" :title="`Card ${i}`" size="small">
          Mobile card {{ i }}
        </data-card>
      </grid-layout>
    </template>
    
    <template #sm="{ size }">
      <!-- Small tablet layout -->
      <grid-layout :cols="2" gap="15">
        <data-card v-for="i in 4" :key="i" :title="`Card ${i}`" size="small">
          Tablet card {{ i }}
        </data-card>
      </grid-layout>
    </template>
    
    <template #md="{ size }">
      <!-- Tablet layout -->
      <grid-layout :cols="2" gap="20">
        <data-card v-for="i in 4" :key="i" :title="`Card ${i}`">
          Tablet card {{ i }}
        </data-card>
      </grid-layout>
    </template>
    
    <template #lg="{ size }">
      <!-- Desktop layout -->
      <grid-layout :cols="4" gap="20">
        <data-card v-for="i in 4" :key="i" :title="`Card ${i}`">
          Desktop card {{ i }}
        </data-card>
      </grid-layout>
    </template>
    
    <template #xl="{ size }">
      <!-- Large desktop layout -->
      <grid-layout :cols="4" gap="25">
        <data-card v-for="i in 4" :key="i" :title="`Card ${i}`">
          Large desktop card {{ i }}
        </data-card>
      </grid-layout>
    </template>
  </responsive-container>
</template>

Breakpoints

BreakpointMin WidthMax Width
xs0575px
sm576px767px
md768px991px
lg992px1199px
xl1200px1599px
xxl1600px

📚 Next Steps

Build SaaS Products Without Limits.