Skip to content
kc@kumarChandrachooda.com:~$ cd /design-system/motion && read --section="01 loaders" 0%
KC Design System — Sketch / Sketch

Rough it out.
One system, every component.

The sketch style redraws the system freehand in design-system-sketch.css: handwriting type, wobble borders, sticky-note labels and marker highlights, everything a degree or two off square. Same components, same markup — the notebook the classic skin was ruled up from.

Specimen

One card, re-inked and re-shaped by whatever the two columns say.

Primary chip

Set — Sketch / Sketch

Motion

Loaders

One waiting vocabulary at four scales, so “the site is booting” and “this button is thinking” never borrow each other’s language. The page entrance is .ds-loader in one of two variants — the counter, a mono percentage over the ten-cell meter at splash scale, or the stamp, the KC plate landing on its accent pop shadow. Both leave the same way: the halves part, as they always have. Below them sit the cell wave for a fetch with no known shape and .ds-btn.is-busy for a control carrying its own wait — the nine banter boxes that used to be the whole loader, kept alive at 7px. The full-screen loader shows once per session and never under reduced motion; the small ones are for waits the reader started, so they appear at once.
The counter — the default. A mono percentage over the ten-cell meter, at splash scale. There is no real progress figure to report, so the count eases toward ninety and the last tenth is spent by whatever actually finishes the load: the meter cannot fill and then sit there, and it cannot claim done before done. Set with ViewData["DsLoader"] = true.
The stamp — the alternative, for a surface that wants the monogram rather than a number. The plate drops from above scale, squares up and lands on the 8px accent pop shadow every floating surface in the system earns, then breathes on the shadow while the page arrives behind it. Add ViewData["DsLoaderStyle"] = "stamp". Both variants leave the same way — the halves part, as they always have.
The cell wave — the segmented meter again, this time with nowhere to get to: six bordered cells passing a periwinkle pulse down the line. It is the loader for a fetch whose shape is not known in advance, where a skeleton would be a promise you cannot keep — a carousel page, a drawer list, a filter re-query. Where the shape is known, use the skeleton shelf instead; it says more.
Loading more
The busy button — the full-screen loader's DNA at 7px. These three are the nine banter boxes that were the page loader, kept alive at the smallest scale the system has. is-busy drops the hover wipe and the press bounce (both of them read as “your click landed”, which is the wrong thing to say while the click is still being honoured) and turns pointer events off; the label stays put, because swapping it for “Loading…” makes the button change width mid-press. The dots take currentColor, so they are white on the solid button and ink on the ghost with no variant rule to keep in sync. This is the state loadMore() applies to the load-more link while a page is in flight.
ScaleHookUse it when
Page.ds-loader--counterThe site is booting. Once per session, never under reduced motion.
Page.ds-loader--stampSame, where the monogram says it better than a number.
Region.ds-skeletonContent is coming and you already know its shape.
Inline.ds-cellwaveContent is coming and you do not.
Control.ds-btn.is-busyA press is being honoured.
Below the page scale, all of them are for waits the reader started, so they appear at once rather than after a beat — the beat belongs to the caller, which should hold off ~300ms before showing anything so a fast response never flashes a placeholder. Every looping animation resolves to a legible static frame under prefers-reduced-motion rather than freezing mid-cycle, and the page loader is skipped entirely.
Motion

Skeleton shelf

The region-scale loader: a card’s own markup with its text swapped for shimmering .ds-skel blocks. Skeletons are never look-alike boxes — they wear the real component’s classes and add .ds-skeleton, so the border, padding and media ratio come from the one definition that already exists and the grid cannot reflow when content lands. Every card in the system has one; _Skeleton renders any of them by name.
A skeleton is the card, emptied. It wears the component's own class list plus .ds-skeleton, keeps every structural wrapper, and swaps only the leaves that held text or an image for .ds-skel blocks. Nothing about the box is restated — border, radius, padding, gaps and the media ratio all come from the one definition that already exists, which is why a skeleton cannot drift from its card. That guarantees the part that matters: the grid does not reflow when content lands, because the column tracks and the media ratios are identical before and after. Height is an estimate, not a promise — the line counts stand in for a typical card, and each bar plus its stack gap is sized to equal the line-height it replaces, so a title that runs long still grows the card by a line. That is the honest limit of the technique. .ds-skeleton also switches off every transition and animation the borrowed classes bring with them: a card that lifts on hover or draws its border in has nothing to respond to yet. Widths are the exception and are set inline — the width is the content, and content is the one thing a skeleton cannot inherit.
Roots are never <a>, <button> or <details>, even where the real component's is: pointer events are off but the tab order is not, and a focusable aria-hidden element is a WCAG 4.1.2 failure. Call one with <partial name="_Skeleton" model="card" />.
flagship — the index's split card. The cover panel keeps .ds-flagship__media, so it disappears below 900px exactly where the real one does; the kicker's accent line stays solid, because it is chrome rather than content and it is what keeps the empty card recognisable.
card — the article card, in the real .ds-grid. The media panel inherits its ratio from the host, which is the reason to shelve skeletons in the component's own grid rather than a generic one: in .ds-grid--3 the same markup is 16/7 instead of 16/10, and the placeholder follows without being told. This is the shape loadMore() appends while the next page is in flight.
post-row — the dense list view. It invents no box: the real row is a top hairline and nothing else, and so is this.
topic — the tier-one navigation tile. Its ink wipe needs no neutralising: it only ever runs on :hover, and a skeleton has no pointer events to hover with.
series-card — the stacked sheets are kept. They are a box-shadow, so they cost no layout, and they are the one thing that says “this is multi-part” before a word of it has arrived. The label tab is the exception to shimmering every leaf: its folded-corner tail and the sweep are the same pseudo-element, so the tab gets its geometry back with --tab and drops .ds-label.
series-item — the numbered part row from a series page.
series-map and pagenav — the in-post panel and the prev/next footer. The map's <details> becomes a plain div, since a skeleton must not open. The pagenav's dashed --placeholder cell is not this and must not be reused as it: that one is a result — “the series ends here” — and borrowing it for a wait would say the opposite of what is true.
author, related and continue — the three panels that close a post and open the index. The continue shelf keeps its progress track and drops the fill: an empty track is the honest zero, and the real fill growing into it is the reveal the component already ships. Its dismiss control keeps its 28px box so the card's right padding still makes sense, but loses the outline that made it read as pressable.
project and cert — both real components carry a <details> deep-dive, and both skeletons delete it rather than disable it: closed is the card's resting height anyway, so the collapsed face is the honest promise.
feature — in the real .ds-intro-grid. The body keeps its 3px accent border; the hover flood needs no neutralising, for the same reason the topic tile's wipe does not.
edu, notecard and skill-group. The notecard is the one card with a coloured ground, and it is the one place the default block colour is wrong: #efefef is lighter than violet while the text it stands in for is much darker, so the card would read as punched full of holes rather than as waiting. It tints the ink instead. Its label tab stays solid, like the flagship's accent line — chrome, not content. The skill group keeps its meter tracks and drops their fills, which is the same trick as the continue shelf's.
Motion

Motion & the slide experience

The original slideshow ideas live on here, rebuilt natively: no jQuery, no owl-carousel, no AOS. Every effect is progressive enhancement and switches off under prefers-reduced-motion. The homepage is a six-panel deck; this guide and the blog are ordinary scrolling documents.
Scroll reveal. Add data-ds-reveal to fade-and-rise an element into view, or data-ds-reveal="stagger" on a container to cascade its children.

Reveal one

Cascades in as the row enters the viewport.

Reveal two

A beat later than its neighbour.

Reveal three

And later still, a gentle stagger.

Carousel. A scroll-snap track (.ds-carousel, optionally --peek) with generated prev/next controls and a position counter. Used for skill groups and certifications.
EffectHookOrigin
Transform-paged deck (vertical panels).ds-deck > .ds-deck__track > .ds-panelcd-slideshow
Horizontal sub-slides.ds-substrip > .ds-subcd-marginal-nav
Floating content card.ds-panel__card.content
Rotated vertical title rail.ds-panel__title.slide-title
Slide number.ds-slidenum.slide-number
Pulsing direction arrows.ds-indicator.cd-single-step
Letter-by-letter title.ds-lettersanime.js ml3
Page loader (once per session).ds-loader — see Loadersbanter-loader
Two-ball cursorauto (fine pointer).cursor__ball
Dark header on scroll.ds-header.is-scrolled.salva-header.sticky