KC Design System — Classic / Flow
Flow with it.
Flow with it.
One system, every component.
The classic skin: Inter typography, ink-black 2px borders and the periwinkle
accent, expressed as CSS custom properties in
design-system.css.
The resume and the blog are both built from these parts.
Design style
Specimen
One card, re-inked and re-shaped by whatever the two columns say.
Primary
chip
Set — Classic / Flow
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.
| Scale | Hook | Use it when |
|---|---|---|
| Page | .ds-loader--counter | The site is booting. Once per session, never under reduced motion. |
| Page | .ds-loader--stamp | Same, where the monogram says it better than a number. |
| Region | .ds-skeleton | Content is coming and you already know its shape. |
| Inline | .ds-cellwave | Content is coming and you do not. |
| Control | .ds-btn.is-busy | A 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.
Panel one
Swipe, drag, or use the controls below.
Panel two
Snaps cleanly to each item.
Panel three
Counter tracks position.
Panel four
Buttons disable at the ends.
| Effect | Hook | Origin |
|---|---|---|
| Transform-paged deck (vertical panels) | .ds-deck > .ds-deck__track > .ds-panel | cd-slideshow |
| Horizontal sub-slides | .ds-substrip > .ds-sub | cd-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-letters | anime.js ml3 |
| Page loader (once per session) | .ds-loader — see Loaders | banter-loader |
| Two-ball cursor | auto (fine pointer) | .cursor__ball |
| Dark header on scroll | .ds-header.is-scrolled | .salva-header.sticky |