Composition Rules
How to combine Byteion components into complex layouts.
Overview
Byteion components are designed to compose. A Card can contain a Table. A Table can contain Buttons. A Modal can contain a Form. A Sidebar can contain navigation links. This guide defines the rules for combining these pieces without creating inconsistent padding, conflicting borders, nested shadows, or broken accessibility.
Composition rules matter because Byteion examples are copied into real commercial projects — SaaS dashboards, admin panels, settings screens, ERP forms, POS panels, ecommerce back offices, internal tools, Laravel starter kits, and custom plugins. A poorly composed example propagates bad patterns across an entire codebase.
The goal is predictable, token-consistent layouts that survive theming, dark mode, and responsive breakpoints without manual fixes.
Composition mental model
Think of every page as a hierarchy of layers. Each layer has a specific responsibility:
Controls max-width, high-level layout (sidebar + main + aside), and padding. Usually one per route.
Groups related content visually, often with a heading. May use space-y-* to separate its children.
Components like Card, Panel, or Table provide background (bg-card), border (border-border), border-radius, and optional shadow (shadow-card).
Controls spacing between children via space-y-*, gap-*, or divide-y. This is where layout happens.
Labels, descriptions, inputs, and actions that form a logical unit like a form field or a metric card.
Buttons, inputs, switches, selects. These are the leaf nodes that handle interaction.
Core rules
1. Let parents control spacing
Use space-y-*, space-x-*, or gap-* on the parent container to create space between children. Do not add margins to individual children — it makes reordering unpredictable and breaks responsive behaviour.
2. Let surfaces control padding
A Card provides padding via its p-5 (or equivalent). Content placed inside does not need additional padding. Exception: full-bleed elements like images, maps, or data tables should break out of padding using negative margins or by rendering outside the padded area.
3. Avoid nested shadows
A Card inside a Card creates double shadows that look muddy and unprofessional. If you need nested surfaces, use bg-muted or bg-card on the inner surface without shadow-card. Reserve shadows for the outermost surface.
4. Use dividers instead of repeated borders
When displaying a list of items inside a surface, use divide-y divide-border on the container instead of giving each item its own border. The surface provides the outer boundary; dividers separate items internally.
5. Do not add random margins to children
Resist the urge to add mb-4 or mt-2 to individual components. This creates inconsistencies when components are reused in different contexts. Instead, wrap them in a Stack (space-y-*) or Grid (gap-*) that controls spacing from one place.
6. Keep form actions in predictable places
Form actions (Submit, Cancel) belong at the bottom of a form, aligned to the end (right in LTR). In modals, actions go in the modal footer. In settings cards, actions go after the fields. Consistency trains user expectations.
7. Keep modal content shallow and focused
A modal should contain one logical task — editing a record, confirming an action, or showing a detail. Use the modal header for title and close, the body for content, and the footer for actions. Do not nest cards inside modals.
8. Use responsive grids intentionally
Use grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-* for card grids, metric dashboards, and form layouts. Choose breakpoints based on content, not arbitrary widths. Test each breakpoint with real content.
9. Prefer semantic tokens over raw colours
Use bg-card, bg-background, text-foreground, text-muted-foreground, border-border, bg-primary, text-primary-foreground, shadow-card. Avoid bg-blue-500, text-white, border-zinc-*, or bg-gray-*. Semantic tokens survive dark mode and theme changes automatically.
10. Avoid unnecessary wrapper divs
Every wrapper div adds DOM weight and cognitive overhead. Do not add a div unless it provides layout (flex, grid, space-y), semantics (role, aria), or behaviour (click handler, data attribute). A bare <div> with no purpose should be removed.
Good and bad examples
Good: Card + Stack + Form
A settings card with proper composition: the Card provides surface styling, space-y-* controls spacing between children, and each form field is a content group with label + input. No individual margins, no nested shadows, no raw colours.
<div class="rounded-xl border border-border bg-card p-5 shadow-card">
<div class="space-y-6">
<!-- Section heading -->
<div>
<h2 class="text-lg font-semibold text-foreground">Profile Settings</h2>
<p class="text-sm text-muted-foreground">Update your personal information.</p>
</div>
<!-- Form fields -->
<div class="space-y-4">
<div>
<label for="name" class="mb-1.5 block text-sm font-medium text-foreground">Full name</label>
<input id="name" type="text" placeholder="Enter your name"
class="w-full rounded-input border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20">
</div>
<div>
<label for="email" class="mb-1.5 block text-sm font-medium text-foreground">Email address</label>
<input id="email" type="email" placeholder="you@example.com"
class="w-full rounded-input border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20">
</div>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-3 border-t border-border pt-4">
<button type="button" class="rounded-button border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted">Cancel</button>
<button type="submit" class="rounded-button bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">Save changes</button>
</div>
</div>
</div>
Bad: Child margins, nested cards, repeated shadows, raw colours
Every child uses mb-* instead of a parent spacer. The outer Card and inner Card both have shadow. The button uses bg-blue-500 text-white instead of semantic tokens. This layout breaks in dark mode, is hard to reorder, and looks visually cluttered.
<!-- BAD: Do not copy -->
<div class="rounded-xl border border-zinc-200 bg-white p-5 shadow-lg">
<h2 class="text-lg font-bold text-zinc-900 mb-4">Profile Settings</h2>
<p class="text-sm text-zinc-500 mb-4">Update your personal information.</p>
<div class="rounded-lg border border-zinc-200 bg-zinc-50 p-4 shadow">
<label class="block text-sm font-medium text-zinc-700 mb-1">Full name</label>
<input class="w-full rounded border border-zinc-300 px-3 py-2 text-sm mb-3">
<label class="block text-sm font-medium text-zinc-700 mb-1">Email</label>
<input class="w-full rounded border border-zinc-300 px-3 py-2 text-sm mb-3">
</div>
<button class="mt-4 bg-blue-500 text-white px-4 py-2 rounded font-medium">Save</button>
</div>
Good: Table page composition
A data table page composed of toolbar, table surface, pagination, and an optional empty or loading state. Each layer has a clear responsibility.
<div class="space-y-4">
<!-- Toolbar -->
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<input type="search" placeholder="Search customers..."
class="w-64 rounded-input border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20">
<select class="rounded-input border border-border bg-background px-3 py-2 text-sm text-foreground outline-none">
<option>All status</option>
<option>Active</option>
<option>Inactive</option>
</select>
</div>
<button class="rounded-button bg-primary px-4 py-2 text-sm font-medium text-primary-foreground">Add customer</button>
</div>
<!-- Table surface -->
<div class="overflow-hidden rounded-xl border border-border bg-card shadow-card">
<table class="w-full text-sm">
<thead class="border-b border-border bg-muted/50">
<tr>
<th class="px-4 py-3 text-left font-medium text-foreground">Name</th>
<th class="px-4 py-3 text-left font-medium text-foreground">Email</th>
<th class="px-4 py-3 text-left font-medium text-foreground">Status</th>
<th class="px-4 py-3 text-right font-medium text-foreground">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<tr class="hover:bg-muted/50">
<td class="px-4 py-3 text-foreground">Jane Cooper</td>
<td class="px-4 py-3 text-muted-foreground">jane@example.com</td>
<td class="px-4 py-3"><span class="inline-flex rounded-full bg-success-50 px-2 py-0.5 text-xs font-medium text-success-700">Active</span></td>
<td class="px-4 py-3 text-right"><button class="text-sm text-primary hover:underline">Edit</button></td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="flex items-center justify-between text-sm text-muted-foreground">
<span>Showing 1 to 10 of 156 results</span>
<div class="flex items-center gap-2">
<button class="rounded-button border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted">Previous</button>
<button class="rounded-button border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted">Next</button>
</div>
</div>
</div>
Good: Modal form composition
A modal with header, body, and footer layers. The body uses space-y-* for form fields. Actions are grouped in the footer. No nested surfaces, no individual margins.
<div role="dialog" aria-modal="true" aria-labelledby="modal-title"
class="fixed inset-0 z-50 flex items-center justify-center bg-background/80">
<div class="w-full max-w-lg rounded-xl border border-border bg-card shadow-card">
<!-- Header -->
<div class="flex items-center justify-between border-b border-border px-6 py-4">
<h2 id="modal-title" class="text-lg font-semibold text-foreground">Edit product</h2>
<button aria-label="Close" class="rounded-button p-1 text-muted-foreground hover:bg-muted">
<i class="ti ti-x text-lg"></i>
</button>
</div>
<!-- Body -->
<div class="space-y-4 px-6 py-4">
<div>
<label for="prod-name" class="mb-1.5 block text-sm font-medium text-foreground">Product name</label>
<input id="prod-name" type="text"
class="w-full rounded-input border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20">
</div>
<div>
<label for="prod-price" class="mb-1.5 block text-sm font-medium text-foreground">Price</label>
<input id="prod-price" type="number" step="0.01"
class="w-full rounded-input border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20">
</div>
<div>
<label for="prod-category" class="mb-1.5 block text-sm font-medium text-foreground">Category</label>
<select id="prod-category"
class="w-full rounded-input border border-border bg-background px-3 py-2 text-sm text-foreground outline-none">
<option>Electronics</option>
<option>Clothing</option>
<option>Food</option>
</select>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-end gap-3 border-t border-border px-6 py-4">
<button type="button" class="rounded-button border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted">Cancel</button>
<button type="submit" class="rounded-button bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">Save product</button>
</div>
</div>
</div>
Practical scenarios
Settings page
Container → Section (heading) → Stack (card grouping) → Card (surface) → Stack (form fields) → Content groups (label + input) → Actions (save / cancel).
Dashboard metric grid
Grid (grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4) → Card (surface with p-4) → Stack (space-y-1) → Label → Value → Trend indicator. Each metric is identical in structure; only the data changes.
Data table page
Page shell → Toolbar (search + filters + add button) → Table surface (border + bg-card + shadow-card) → Table head + body + empty/loading state → Pagination row. The table surface provides boundaries; dividers separate rows internally.
Modal form
Backdrop → Modal surface (rounded-xl + border + bg-card + shadow-card) → Header (title + close) → Body (space-y-* form fields) → Footer (actions). Do not nest additional cards inside the modal body.
Ecommerce product edit page
Page shell → Two-column grid (grid-cols-1 lg:grid-cols-3 gap-6) → Main column (span 2): Card with form fields, media uploader, description editor. Side column (span 1): Card for status, visibility, publish date. Each Card is an independent surface with its own space-y-* layout.
ERP / POS data entry screen
Page shell → Top toolbar (search product, scan barcode) → Main area: left panel (product grid, Card surface) + right panel (cart/order summary, Card surface) → Bottom bar (total, tax, pay button). Each surface is self-contained with its own spacing system.
Do and Don't
Do
- Use
space-y-*orgap-*for spacing between children - Let Card / Panel control surface styling (
bg-card,border-border,shadow-card) - Use semantic tokens everywhere
- Keep form actions grouped and in predictable places (bottom-right in LTR)
- Keep nested layouts shallow — 3 levels maximum (Surface → Stack → Control)
- Use
divide-y divide-borderfor repeated rows inside a surface - Test every layout in dark mode and at mobile breakpoints
Don't
- Add
mb-*ormt-*to every child — use parent-controlled spacing - Stack multiple shadows (
shadow-cardinsideshadow-card) - Put borders on every repeated item — the surface provides the boundary
- Hardcode raw colours (
bg-blue-500,text-white,border-zinc-*) - Add wrapper divs that do not provide layout, semantics, or behaviour
- Nest modals inside modals
- Put full-page padding inside a Card — use the page shell or section
Developer tips
Start with the outer layout. Before placing any component, define the page shell, sections, and grid. Components then fit into the slots.
Inspect spacing from parent to child. If you see unexpected space, open the browser inspector and check which element is adding margin or padding — it is almost always a child margin that should be removed.
Use one surface per visual group. If a group of elements needs a background, border, or shadow, wrap them in a single Card. Do not give each element its own surface.
Use dividers for repeated rows. Instead of adding border-b to every row in a list, use divide-y divide-border on the parent container.
Test responsive behaviour early. Build the mobile layout first, then layer on larger breakpoints. This prevents overflow and stacking issues.
Keep examples copy-paste friendly. Every example on this page uses only Byteion semantic tokens and standard Tailwind utilities. You can paste them directly into a Byteion project.
Use the same input classes everywhere. Copy this production-ready input class string: w-full rounded-input border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20.
Buttons always use text-primary-foreground on bg-primary. Never use text-white — it breaks in custom themes.
Composition is hierarchical. Layout primitives (Grid, Stack) wrap surface components (Card, Panel), which wrap content groups (label + input), which wrap controls (Button, Input, Select). Respect this hierarchy.
Related
- Design Principles — the philosophy behind Byteion tokens and components
- Spacing — detailed spacing scale and usage
- Dashboard Layout — page shell, grid, and responsive patterns
- Card — the primary surface component
- Form Field — form layout and field grouping
- Table — data table structure and composition
- Modal — modal header, body, footer structure
- Theming Guide — customising tokens and dark mode
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.
Explore Pro Components