asciileton
v1.1.0
Published
React skeleton loader based on a simple string draft language
Maintainers
Readme
Asciileton – React SVG Skeleton Loader
Overview
Asciileton is a tiny React component that turns a draft string – a custom, human‑readable language – into a set of SVG‑styled skeleton blocks (squares or circles). The component parses the draft, expands groups and repetitions, and renders the corresponding <div> elements with Tailwind‑like dimensions and a pulsing animation.
Draft language syntax
The draft language is deliberately simple – it describes a vertical stack of placeholder blocks. Each block can be a square or a circle, may have an optional width, a required height, and can be repeated or grouped.
| Symbol | Meaning |
|--------|---------|
| [ … ] | Square block |
| ( … ) | Circle block |
| -N- | Aspect‑ratio mode (inside brackets). A single number defines the size in shorthand units (multiplied by 10, same rule as width). Width and height are both set to that value, so the block is always a perfect square (50 → 50×50, 60 → 60×60, etc.). The shape is decided by the surrounding brackets. Empty - means “use the default size (60)”. No x is used, which is the whole point of this shorthand. |
| --…-- | Width × height mode (legacy, inside brackets). See width / height rows below for the inner syntax. |
| width | Inside --…--: optional width. If omitted the default is a random integer in [10, 90]% (digit form [1, 9] with shorthand applied). A single number (50) is a fixed percentage. A closed range (20..60) and an open range (5.. → [50, 90], ..3 → [10, 30]) are also supported. Shorthand: any single‑digit width value (< 10) — including the default bounds 1 and 9 — is multiplied by 10 (e.g. 5 → 50%, 3..7 → 30..70%, ..3 → 10..30%). |
| start..end | Optional width expressed as an inclusive range (e.g. 20..60). At render time a random integer between start and end is picked and used as a percentage. Mutually exclusive with width. |
| x | Separator between width and height inside the legacy --…-- syntax. Not used in the new -N- mode. |
| height | Height expressed as pixels (same shorthand as width). A single‑digit value (< 10) is multiplied by 10 — e.g. 5 → 50px, 9 → 90px; a two‑digit value is taken as‑is — e.g. 22 → 22px, 100 → 100px. The number goes straight into the CSS height (no Tailwind‑scale multiplier). |
| start..end | Optional height expressed as an inclusive range in the same shorthand (e.g. 1..6 → [10, 60]px, 5..22 → [50, 22]px clamped at render time). At render time a random integer between the two widened values is picked and used as the pixel height. Mutually exclusive with height. |
| (space) | Default horizontal separator. A single space (or any whitespace run) between two elements is treated as | — elements sit on the same line with the default horizontal margin. Use this when you just want the elements side‑by‑side. |
| + or +N | Forces the next block to start on a new line. Without a number the margin‑top is 16px (2 × 8px); with a number N the margin‑top becomes N × 8px. |
| \| or \|N | Horizontal separator — the next block stays on the same line but gains a left margin. Without a number the margin is 16px (2 × 8px); with a number N it becomes N × 8px. |
| = or =N | Center alignment — the next block stays on the same line and gains a left margin (default 16px, N × 8px), and forces the entire row to be centered horizontally. |
| *N | Repeats the preceding element N times (e.g. (-6-)*3 or [--x6--]*3). The copies are placed on the same line (horizontal row) — use +*N if you want a vertical stack instead. Has higher precedence than +, so [--a--]+[--b--]*10 produces 11 elements (1 + 10), not 11 copies of the sum. |
| +*N | Repeats the last element N times with a line break (margin‑top) between each copy (e.g. (-6-)+*10 produces 10 copies of the circle, each on its own line). Combines the meaning of + and *N in one operator. |
| ( … )*N | Group – a sequence of blocks containing at least one + (line break). After expansion the group is inserted N times, with each repetition separated by a +. The group regex requires + inside, so (-6-)*10 is treated as a single element with repetition (not a group). |
Layout
- Elements without any separator sit horizontally (side‑by‑side in a single row).
(-4-)*3drawsO O O,[--3x5--]*3draws▢ ▢ ▢. +(or+N) breaks the row — the next element starts on a new line with a vertical margin.|(or|N) and a single space between elements add a horizontal margin within the same row.=(or=N) centers the row — all elements in the current line are centered horizontally.
Grammar (informal)
Draft ::= Item ( "+" Item )*
Item ::= Block | Group
Block ::= Square | Circle
Square ::= "[" Size "--]"
Circle ::= "(" Size "--)"
Size ::= "--" [Width "%"] ["x"] Height "--"
Width ::= DIGITS
Height ::= DIGITS
Group ::= "(" Draft ")" ["*" Number]
Number ::= DIGITS- Width is optional. If omitted the default is a random integer in
[10, 90]%(digit form[1, 9]after the shorthand). It can be a single number (50), a closed range (20..60), or an open range (5..→[50, 90],..3→[10, 30]). Shorthand: any single‑digit width value (< 10) — including the default bounds1and9— is multiplied by10—5→50,3..7→30..70,5..→50..90. - Height is mandatory and determines the vertical size (
height * 8px). It can be either a single number (6) or a range (1..6). - The
+token does not create a visual element; it adds a top margin to the following block. If written as+the margin is 24px;+Nsets the margin toN × 8px. - Repetition (
*N) expands the element before rendering – the parser creates N identicalSkeletonItemobjects.
Examples
import Asciileton from "./Asciileton"
// Simple square, full width, height = 6 (=> 48px)
<Asciileton draft="[--x6--]" />
// Circle with explicit width 30 and height 4
<Asciileton draft="(--30x4--)" />
// Circle with a random width between 20 and 60 percent
<Asciileton draft="(--20..60x4--)" />
// Square with a random height between 1 and 6 units (8..48px)
<Asciileton draft="[--x1..6--]" />
// Same range as `[--30..70x6--]` thanks to the shorthand for values < 10
<Asciileton draft="[--3..7x6--]" />
// No width → random width in [1, 9]%
<Asciileton draft="[--x6--]" />
// Open upper bound → range [5, 9]%
<Asciileton draft="[--5..x6--]" />
// Open lower bound → range [1, 3]%
<Asciileton draft="[--..3x3--]" />
// Two squares on separate lines, second one is 50% wide
<Asciileton draft="[--x6--]+[--50x6--]" />
// Repeating a block 3 times
<Asciileton draft="[--x6--]*3" />
// Aspect‑ratio mode (new): a single number defines the size, no `x`.
// `-6-` → 60px radius shorthand, so 60×60 circle.
<Asciileton draft="(-6-)" />
// Same idea for a square — 60×60 (no rounded corners)
<Asciileton draft="[-6-]" />
// Empty body uses the default size (60)
<Asciileton draft="(--)" />
// Horizontal layout: 3 circles in a single row
<Asciileton draft="(-4-)*3" />
// Same for the legacy syntax: 3 squares side-by-side
<Asciileton draft="[--3x5--]*3" />
// Whitespace between elements is the default horizontal separator
<Asciileton draft="(-3-) (-4-)" />
// Explicit `|` for horizontal spacing, `|N` for custom spacing
<Asciileton draft="(-3-)|3(-4-)" />
// Centered elements in a row
<Asciileton draft="(-6-)=(-6-)" />
// `+` forces a new line (default margin 16px), `+N` for custom
<Asciileton draft="(-6-)+3[-6-]" />
// Combinations
<Asciileton draft="(-12-)+[-6-]*3" />
// Group with line‑breaks repeated 2 times
<Asciileton draft="([--x6--]+[--30x6--])*2" />The last example expands to:
[--x6--] + [--30%x6--] + [--x6--] + [--30%x6--]resulting in four squares, each pair separated by a vertical margin.
Installation
npm install asciileton --save-peer reactUsage
import { Asciileton } from "asciileton"
function MyComponent() {
const draft = "(--30x6--)+[--50x4--]*2"
return <Asciileton draft={draft} className="w-full" />
}// Asciileton.tsx is exported as default
import Asciileton from "./Asciileton"
function ProfileLoading() {
const draft = "(--30x6--)+[--50x4--]*2"
return <Asciileton draft={draft} className="w-full" />
}You can style the outer wrapper with the optional className prop or wrap the component in any layout container.
How the parser works (quick overview)
- Pre‑processing – whitespace around operators
+,|,*is stripped. Any whitespace run between a closing bracket ()or]) and an opening bracket ((or[) is replaced by a single|. Remaining whitespace is removed. The result is equivalent to the user having written|between elements that were separated by a space. - Group expansion – the regex
/\(([^()]*\+[^()]+)\)\*(\d+)/grepeatedly replaces(content)*Nwithcontent+content+…(N times). The regex requires+inside the parens, so(-6-)*10is not treated as a group – it stays as a single element that is later repeated by the per‑element*Nlogic. - Tokenisation – the string is split on
+,|,*and the bracket boundaries, preserving each element token. - Element parsing – each token that matches
^[\[\(].+?[\]\)]$is interpreted:- Aspect‑ratio mode (new) – if the body matches
^[([](-(\d+)?-)[)\]]$, a single number setsradius(in shorthand‑pixels). Width and height are both equal to the radius. The shape comes from the brackets ((→ circle,[→ square). - Legacy mode – otherwise the body is matched against
--…WIDTHxHEIGHT…--, yielding optional width (single50, closed range20..60, open ranges5../..3, or default[10, 90]) and mandatory height (6or1..6). +(or+N) setsmarginTopof the next element and starts a new line;|NsetsmarginLeftof the next element (default 16px,N × 8pxotherwise). The numberNis consumed from the following token, so+N*Mworks too.- A trailing
*Nrepeats the element N times on the same line.
- Aspect‑ratio mode (new) – if the body matches
- Rendering – items are grouped into rows by
marginTop(each row is a flex‑row container; the first row has no top margin, subsequent rows use the first item’smarginTop). Within a row,marginLeftprovides horizontal spacing. Ifradiusis set:width: height: radius,border-radius: 9999pxfor circles or none for squares. Otherwise:height: <units> * 8px,width: <resolved>%,border-radius: 9999pxfor circles or none for squares. A static@keyframespulse animation is applied.
License
MIT – feel free to copy, modify, and publish.
