Architecture Deep Dive
This page covers the internal design decisions and algorithms behind AutoSkeleton.
Leaf-Only Replacement
AutoSkeleton follows a leaf-only replacement strategy:
- Container elements (divs with children, flex/grid parents) are preserved as-is. Their layout properties (display, gap, padding, flex-direction) are copied to the skeleton.
- Leaf elements (text, images, buttons, inputs) are replaced with skeleton blocks.
This means the skeleton inherits the exact same layout as the real content — same gaps, same alignment, same grid structure.
What counts as a "leaf"?
An element is skeletonized as a leaf if any of these are true:
- Tag is
IMG,BUTTON,INPUT,TEXTAREA,SELECT, orSVG - Has text content and no child elements
- Is a single-child text container (e.g.,
<p><span>text</span></p>)
An element is kept as a container if:
- Has more than 1 child element
- Has
display: flexordisplay: grid - Is a table structural element (
TABLE,THEAD,TBODY,TR)
Wrapper + Content Pattern
Each skeleton block is wrapped in an outer element that preserves spacing:
┌─ Outer Wrapper ──────────────────┐│ margin, padding, flex props ││ ┌─ Inner Skeleton ──────────┐ ││ │ width, height, color │ ││ │ borderRadius, animation │ ││ └───────────────────────────┘ │└──────────────────────────────────┘The outer wrapper carries:
margin,paddingflex,flexGrow,flexShrink,flexBasisalignSelf,justifySelfgridColumn,gridRow,gridArea
The inner skeleton carries:
width,heightbackgroundColorborderRadiusanimation
This separation ensures skeletons maintain exact spacing without the inner block affecting layout.
Score-Based Role Inference
Each element is scored across multiple roles. The highest score wins.
Scoring Table
Text
| Signal | Points |
|---|---|
| Has non-empty text content | +40 |
Height between minTextHeight and minTextHeight * 3 | +30 |
Tag is P, SPAN, H1-H6, LABEL, A, DIV | +20 |
| Font size between 0-100px | +20 |
Image
| Signal | Points |
|---|---|
Tag is IMG | +100 |
Has role="img" attribute | +60 |
Has CSS background-image (not none) | +50 |
Area > minImageSize^2 (default 1024px^2) | +30 |
Icon
| Signal | Points |
|---|---|
Tag is SVG | +70 |
Area < iconMaxSize^2 AND aspect ratio 0.8-1.2 (square-ish) | +40 |
Button
| Signal | Points |
|---|---|
Tag is BUTTON | +80 |
Has role="button" attribute | +60 |
Has cursor: pointer AND area < 20,000px^2 | +30 |
Input
| Signal | Points |
|---|---|
Tag is INPUT, TEXTAREA, or SELECT | +80 |
Has contenteditable attribute | +50 |
Skip (spacer elements)
| Signal | Points |
|---|---|
| Area < 100px^2 | +50 |
| Height < 5px OR width < 5px | +40 |
Selection Logic
- Each role's signals are summed
- The highest-scoring role wins
- Minimum threshold: 30 points — below this, defaults to
text - Container role is handled separately (not part of scoring)
Multi-Line Text Detection
AutoSkeleton detects multi-line text blocks and renders them as multiple skeleton bars:
Algorithm
- Get
lineHeightfrom computed styles - If
lineHeightis'normal', calculate asfontSize * 1.2 - If
height > lineHeight:lines = Math.ceil(height / lineHeight) - Otherwise:
lines = 1
Rendering
- Each line is a separate skeleton bar
- The last line is 70% width for a natural text appearance
- Gap between lines =
lineHeight - singleLineHeight(minimum 4px)
████████████████████████████████████████ <- line 1 (100% width)████████████████████████████████████████ <- line 2 (100% width)██████████████████████████████ <- line 3 (70% width)Table Structure Preservation
Tables are handled specially to maintain their grid structure:
Structural Tags (never skeletonized)
TABLE, THEAD, TBODY, TFOOT, TR
These are rendered as actual HTML table elements in the skeleton. Their children are processed recursively.
Cell Tags (cell preserved, content skeletonized)
TH, TD
The cell element is preserved with its styles (padding, borders, background). The cell's children are skeletonized as leaf elements.
Why not just replace the whole table?
A single skeleton block can't represent column widths, header styles, or row counts. By preserving the table structure, the skeleton accurately shows:
- Number of columns and their widths
- Header vs. body distinction
- Row count
- Cell padding and borders
Overlay Architecture
The AutoSkeleton component renders three layers:
<div style={{ position: 'relative' }}> {/* Layer 1: Content (always rendered) */} <div className="auto-skeleton-content"> {children} </div> {/* Layer 2: Skeleton overlay (absolute positioned) */} <div style={{ position: 'absolute', top: 0, left: 0, right: 0 }}> <SkeletonRenderer blueprint={blueprint} /> </div> {/* Layer 3: Measurement container (invisible) */} <div style={{ opacity: 0, position: 'absolute', pointerEvents: 'none' }}> {children} </div></div>Layer 1 (Content): Always in the DOM. Hidden via visibility: hidden during loading. This prevents layout shifts when content appears.
Layer 2 (Skeleton): Positioned absolutely on top. Fades out over 300ms when loading ends. Unmounted after fade.
Layer 3 (Measurement): A hidden copy of children used for DOM measurement. Only present during loading. Completely invisible (opacity: 0).
Why this approach?
- No layout shift: Content is always rendered, just hidden. Dimensions are stable.
- Smooth transitions: Skeleton fades out while content fades in simultaneously.
- Opt-out support:
visibility: hiddenon the content layer can be overridden by children withvisibility: visible(unlikeopacity, which is multiplicative).
Image Handling
Images with empty or unloaded src attributes get special treatment:
- Detect empty/loading state:
srcis empty,naturalWidth === 0, or src resolves to the current page URL - Calculate intended dimensions from:
- Inline
style.width/style.height - HTML
width/heightattributes - Parent element width (for percentage-based sizes)
- Use calculated dimensions instead of measured dimensions
This ensures image skeletons have the correct size even before the image loads.