Přejít na obsah
en – Switch to English

octoloader

Pixelartová chobotnice, která plave, zatímco čekáš. Indikátor načítání bez jediné závislosti, pro React i čisté HTML. Místo spinneru se řídí tím, jak dlouho se čeká.

Sólový projekt — koncept, pixel art, TypeScript, dokumentace2026Open source (MIT) · na npm · v0.1.1
TypeScriptReact 17+Custom ElementsShadow DOMSVGCSS steps()Zero dependenciesnpm

octoloader je indikátor načítání, ve kterém někdo bydlí. Pět ručně nakreslených póz ramen, každá 30×24 pixelů, se přehrává jako osmisnímkové plavání. Tělo se zvedne, když chobotnice ramena stáhne, a klesne, když je rozevře. K tomu mrknutí, pohled stranou a tři bublinky — každé na vlastních hodinách. Žádný canvas, žádný GIF, žádné závislosti. Sprite tvoří obyčejné SVG cesty, takže je ostrý v jakékoli velikosti a v monochromatické variantě přebírá currentColor.

Víc času než chobotnice zabralo chování. Řídí se Primerovým vzorem pro načítání: první vteřinu se neukáže vůbec nic, od jedné do tří vteřin chobotnice plave bez udaného průběhu a potom nastoupí lišta průběhu. Vychází jako React komponenta i jako custom element se shadow rootem, má 5,2 kB po gzipu a přístupnost je v ní promyšlená, ne dolepená na konec: sprite je skrytý, mluví za něj obal a předat aria-label je typová chyba s odkazem na správnou prop.

5,2 kB
po gzipu, nula závislostí
8
snímků z pěti nakreslených póz
6
časovačů najednou, nikdy v zákrytu
3
tóny v monochromu — se dvěma by to byla lebka

spinner není stav načítání

Spinner uprostřed stránky sděluje jedinou věc: něco se děje. Neřekne, co se načítá, kam obsah dorazí, ani jak dlouho to asi potrvá. A když se objeví na 200 ms, což je doba, kterou většina requestů skutečně trvá, nepůsobí jako postup, ale jako probliknutí.

Indikátor je proto jen polovina balíčku. Druhou polovinou je panel: tenký rámeček, zaoblené rohy a vzduch kolem, roztažený přes celý kontejner, ne jen kolem chobotnice. Vymezuje oblast, kam obsah dorazí — tedy stejné místo, které zaplní prázdný stav, když čekání skončí naprázdno. Tvar je záměrně stejný jako Empty v shadcn nebo Blankslate v Primeru: jedna oblast by neměla měnit velikost a zaoblení podle toho, v jakém je zrovna stavu.

indikátor podle délky čekání

Primer rozlišuje čtyři časová okna a propy jim odpovídají jedna ku jedné. Do vteřiny: nic neukazuj — delay={1000} vrací null, dokud neuplyne. Jedna až tři vteřiny: plavání bez průběhu. Tři až deset: lišta průběhu, pokud ho umíš spočítat. Přes deset: lišta průběhu, a přestaň blokovat stránku.

delay je prop, kterou se vyplatí zapojit všude. Právě ona dělá rozdíl mezi stránkou, která působí svižně, a stránkou, která bliká. Dvě věci ale za tebe neudělá. Nezarezervuje místo, které ještě nezabírá — dej slotu min-height. A neudrží se na obrazovce dost dlouho: loader, který zmizí 80 ms po tom, co se objevil, blikne stejně ošklivě jako ten ukázaný na 200 ms. Stav, který ho řídí, by ho proto měl nechat viditelný nejméně ~400 ms.

lišta průběhu má 28 celých pixelů, takže se posouvá po viditelných krocích

jak se hýbe

Pět póz ramen se přehrává tam a zpátky a vnitřní rameno jde o krok za vnějším. Právě tohle zpoždění dělá ze dvou mávajících končetin jednu postupující vlnu. Celé zvíře se při stažení ramen zvedne a při jejich rozevření klesne, stejně jako když se chobotnice odráží tryskem. Bez toho zdvihu by z osmi snímků zbyl jen listovací obrázek.

Obojí se dá snadno rozbít tak, že to vypadá spíš jako cuknutí než jako chyba, a proto obojí hlídají testy. Pózy jsou nakreslené, ne dopočítané: tweening mezi dvěma pózami, které dělí jediný pixel, se kvůli zaokrouhlování rozpadne a z pěti kroků zbydou tři kresby, zatímco tělo se hýbe dál. A výška patří k póze, ne k číslu snímku. Když stejná póza dostane cestou tam jinou výšku než cestou zpátky, smyčka zadrhává, i když každý snímek sám o sobě sedí.

$osm snímků z pěti póz
snímek   1     2     3     4     5     6     7     8
póza     0     1     2     3     4     3     2     1
zdvih    0px   0px   1px   1px   2px   1px   1px   0px

plavání   0,9 s               cyklus ramen a zdvih
mrknutí   4,6 s               oči zavřené na 115 ms
pohled    7,3 s               zorničky o pixel doleva, později doprava
bublinky  2,6 s 3,2 s 3,8 s   tři, stoupají podél levého boku
Osm snímků plavání vedle sebe: ramena se otevírají a zavírají, tělo je uprostřed cyklu o pixel nebo dva výš
osm snímků a dva pixely zdvihu, díky kterým chobotnice plave, a ne jen mává

šestery hodiny, které se nikdy nesejdou

Plavání běží na 0,9 s, mrknutí na 4,6 s, pohled stranou na 7,3 s a tři bublinky na 2,6, 3,2 a 3,8 s. Všech šest se znovu sejde zhruba jednou za 138 dní — a přesně o to jde: nikdo, kdo kouká na načítací obrazovku, neuvidí celý vzorec proběhnout dokola. Smyčka, kterou se dá naučit, přestává být postavou a stává se ukazatelem průběhu s obličejem.

Snímky leží vedle sebe v jednom dlouhém pásu, který se pomocí steps() posouvá pod viewBoxem vždy o jeden snímek. Nabízí se i jiná cesta — naskládat snímky na sebe a prolínat jim průhlednost — jenže ta stojí jednu animaci na každý snímek každého výrazu. Na stránce s několika loadery jde o stovky animací a projeví se to trhaným pohybem. Takhle je to osm animací bez ohledu na počet kreseb a žádný snímek nikdy nezůstane v půlce výměny. A protože se viewBox nehýbe, můžou oči ležet mimo pás: jedna sada víček a zorniček poslouží pro kterýkoli právě zobrazený snímek.

monochrom má tři tóny, ne dva

Nabízí se udělat monochrom jednou barvou a oči z ní vyseknout. Na bílé stránce to vypadá správně, na černé se z toho stane lebka: tělo zesvětlá, vyseknuté oči ztmavnou spolu s pozadím a díváš se do prázdných důlků.

Proto currentColor přebírá jen tělo. Oči a přísavky mají barvu --octo-paper, zorničky --octo-pupil. Když zůstane oko světlejší než tělo a zornička tmavší než oko, čte se obličej stejně na světlém i tmavém podkladu a jeden sprite pokryje obojí. Lišta průběhu stojí na týchž třech tónech — rámeček v barvě inkoustu, dráha v barvě papíru, výplň v barvě zorničky. Stačí dva z nich sloučit a lišta přestane ukazovat hodnotu. Test hlídá, aby se všechny tři v obou paletách lišily, protože přesně tahle změna ve světlém režimu vypadá v pořádku a v tmavém se rozbije.

tentýž sprite, jen jiná paleta — a oko v monochromu zůstává světlejší než tělo

přístupnost a kde končí

Sprite je aria-hidden, takže se nic nečte dvakrát, a mluví za něj obal. Bez hodnoty průběhu má role="status", s ní role="progressbar" a trojici aria-value*. V obou případech se jeho název bere z popisku, který lidé skutečně vidí, takže se vyslovený a viditelný název nemůžou rozejít. Pokud se lišit opravdu mají, použij announce.

Atributy, které si komponenta odvodí sama, v typu propů vůbec nejsou — nic se potichu nepřepisuje. Předat aria-label je proto chyba při kompilaci, která tě odkáže na announce. Dvě věci zůstávají na tom, kdo komponentu používá, a dokumentace to říká otevřeně, místo aby hádala: aria-busy patří na oblast, jejíž obsah se mění, ne na loader, a jakmile nový obsah dorazí, má se do něj přesunout fokus.

Při prefers-reduced-motion se plavání, mrknutí, pohled i bublinky zastaví a chobotnice jen jemně pulzuje na místě. Že se úplně nezastaví, je záměr — Primer chce, aby animace načítání byla nenápadná, ne aby zmizela, protože zamrzlý indikátor nejde odlišit od zaseknutého. Po obrazovce se ale už nic nepohybuje.

jak chobotnici upravit

src/sprite.ts je samotná kresba — doslova obrázek z textu. Každý snímek je pole řetězců, jeden znak na pixel: . průhledná, o obrys, b tělo, w bělmo, c duhovka, p zornička, g přísavka. Změníš znak, změníš chobotnici. Renderer při importu spojí úseky stejných znaků do jedné SVG cesty pro každou barvu, takže mezi tebou a pixely nestojí žádný build.

Testy hlídají kresbu, nejen kód. Každý snímek musí být úplný obdélník, smí obsahovat jen znaky, které znají obě palety, a uvnitř siluety nesmí zůstat uzavřený pixel pozadí. To poslední zachytí zatoulanou dírku, která je při dvanáctinásobném zvětšení neviditelná a ve skutečné velikosti vypadá jako smítko. Další testy hlídají tvar animace: dva po sobě jdoucí snímky nesmí být stejné, ramena musí mávnout tam a zpátky, ne kroužit jedním směrem, a monochromatická paleta se musí rozložit na tři odlišné tóny.

velikost a výkon

5,2 kB po gzipu pro React, 5,4 kB pro custom element. Žádné runtime závislosti; React je volitelný peer, takže import octoloader/element na stránce bez Reactu nepřitáhne nic navíc. Jeden stylesheet bez ohledu na počet loaderů — vykresluje se jako hoistovaný <style>, který React 19 přesune do <head> a odstraní duplicity. Komponenta je obalená v memo, takže loader uvnitř něčeho, co se překresluje při každém stisku klávesy, nenutí React znovu procházet jeho dvacítku SVG uzlů.

Žádný polyfill, žádná záložní cesta — není kam ustoupit. Bez animace zbude nehybná chobotnice, a ta pořád říká „čeká se“.