Layout

Two levels. Stack, Cluster and Container are the opinionated primitives for arrangements that recur. The flex, grid and spacing utilities are the escape hatch for one-off application layout - reach for a primitive first, drop to utilities when the primitive would need three overrides to say what you meant.

Reference

flex utilities

A Tailwind-shaped subset. Two deliberate differences: the gap scale is the design system's space scale rather than an arbitrary ramp, and there are no responsive or state prefixes - generating those combinatorially is a build tool's job, and this repository has no build tool beyond a file concatenator.

<div class="sk-flex sk-items-center sk-justify-between sk-gap-3">
  <span>left</span>
  <span>right</span>
</div>
AttributeTypeDefaultNotes
sk-flexsk-flex | sk-inline-flex-display
sk-flex-{dir}row | row-reverse | col | col-reverserowflex-direction
sk-flex-{wrap}wrap | wrap-reverse | nowrapnowrapflex-wrap
sk-items-{x}start | center | end | baseline | stretchstretchalign-items. sk-items-end is what a button next to a field needs.
sk-justify-{x}start | center | end | between | around | evenlystartjustify-content
sk-content-{x}start | center | end | between | stretch-align-content, for wrapped multi-line flex
sk-self-{x}auto | start | center | end | stretch | baseline-align-self, set on the child
sk-gap-{n}0-80Maps to --sk-space-{n}. sk-gap-x-{n} and sk-gap-y-{n} set one axis.
sk-flex-{n}1 | auto | initial | none-the flex shorthand
sk-grow, sk-grow-0--flex-grow
sk-shrink, sk-shrink-0--flex-shrink
sk-min-w-0--The fix for an overflowing child in a flex row, every time.

sk-stack

Vertical rhythm.

<div class="sk-stack" data-gap="4">
  <div></div>
  <div></div>
</div>
AttributeTypeDefaultNotes
data-gap0-84Space scale index. Omit for the default.

sk-cluster

A horizontal group that wraps - button rows, tag lists, toolbars.

<div class="sk-cluster" data-gap="3" data-align="end">
  <div></div>
  <div></div>
</div>
AttributeTypeDefaultNotes
data-gap0-83Space scale index.
data-align"start" | "end"centeralign-items
data-justify"center" | "end" | "between"startjustify-content

sk-container

Page width.

<div class="sk-container" data-width="lg"></div>
AttributeTypeDefaultNotes
data-width"sm" | "md" | "lg" | "xl""lg"32 / 48 / 64 / 80rem

sk-visually-hidden

Keeps content in the accessibility tree but off the screen.

<span class="sk-visually-hidden">Opens in a new window</span>

grid utilities

The one arrangement flex genuinely cannot do: a set of equal cards that reflows. A flex row leaves the last item stretched across its own line or a ragged gap at the end; grid with auto-fit goes from four columns to one and keeps every cell the same size, with no media query - which matters here, because this repository cannot generate responsive prefixes. The gap and alignment utilities are not repeated for grid: gap, align-items and justify-content mean the same thing in both, so sk-gap-4 already works.

<div class="sk-grid sk-grid-fit sk-gap-4">
  <div class="sk-card">…</div>
</div>
AttributeTypeDefaultNotes
sk-grid-cols-{n}1-6-minmax(0, 1fr), not 1fr. A track's default minimum is min-content, so one long unbroken string makes its column wider than its share and pushes the grid past its container.
sk-grid-fit-14remauto-fit with a minimum column width. min(…, 100%) on that minimum is load-bearing: without it a 14rem minimum inside a 12rem sidebar overflows instead of reflowing.
sk-grid-fit-sm / -lg9rem / 20rem-The same thing at two other minimums, via --sk-grid-min.
sk-col-span-{n}2 | 3 | 4 | 6-Spans, never absolute line numbers - a line number breaks the moment the column count changes, which for a reflowing grid is every resize.
sk-col-full--1 / -1: the full width of whatever the grid currently is. The only span that survives a track count change.
sk-justify-items-{x}start | center | end | stretchstretchThe one alignment axis flex has no equivalent for. sk-justify-self-{x} does the same per item.

spacing utilities

Margin and padding on the space scale, in logical properties: sk-mt is margin-block-start and sk-ms is margin-inline-start, so a layout flips correctly in a right-to-left document. REACH FOR GAP FIRST - this system zeroes every margin in the reset so that the space between two things belongs to their container, which is what lets a component be dropped into a layout it has never seen. These are for what gap cannot say: padding, centring a fixed-width block, one deliberate exception in an even run, and space around an element whose container you do not control.

<div class="sk-p-4 sk-mb-5">…</div>
<div class="sk-mx-auto">centred</div>
AttributeTypeDefaultNotes
sk-p-{n}, sk-px, sk-py0-8-padding, padding-inline, padding-block.
sk-pt / sk-pb / sk-ps / sk-pe0-8-block-start, block-end, inline-start, inline-end.
sk-m-{n} and friends0-8-The same seven shapes for margin.
sk-mx-auto--The reason margin utilities exist at all in a system built on gap: auto margins are how a fixed-width block centres itself, and no amount of gap on a parent does that job.

Examples

Reflowing grid

Drag the preview narrow: four columns, then three, two, one. No media query anywhere - the same class does all of it.

Kiwi Kart

Eight tracks, four islands, one very determined bird.

Fern & Forge

Build a workshop, fill it with apprentices, never finish anything.

Tui Trader

A trading sim about being slightly too honest.

Moa Mountain

Climb it. That is the whole game.

Pounamu

A puzzle game carved out of one rock.

Show markup
<div class="sk-grid sk-grid-fit sk-gap-4">
  <div class="sk-card">
    <div class="sk-card-body">
      <div class="sk-flex sk-flex-col sk-gap-2">
        <h3 class="sk-card-title">
          Kiwi Kart
        </h3>
        <p class="sk-card-subtitle">
          Eight tracks, four islands, one very determined bird.
        </p>
      </div>
    </div>
  </div>
  <div class="sk-card">
    <div class="sk-card-body">
      <div class="sk-flex sk-flex-col sk-gap-2">
        <h3 class="sk-card-title">
          Fern &amp; Forge
        </h3>
        <p class="sk-card-subtitle">
          Build a workshop, fill it with apprentices, never finish anything.
        </p>
      </div>
    </div>
  </div>
  <div class="sk-card">
    <div class="sk-card-body">
      <div class="sk-flex sk-flex-col sk-gap-2">
        <h3 class="sk-card-title">
          Tui Trader
        </h3>
        <p class="sk-card-subtitle">
          A trading sim about being slightly too honest.
        </p>
      </div>
    </div>
  </div>
  <div class="sk-card">
    <div class="sk-card-body">
      <div class="sk-flex sk-flex-col sk-gap-2">
        <h3 class="sk-card-title">
          Moa Mountain
        </h3>
        <p class="sk-card-subtitle">
          Climb it. That is the whole game.
        </p>
      </div>
    </div>
  </div>
  <div class="sk-card">
    <div class="sk-card-body">
      <div class="sk-flex sk-flex-col sk-gap-2">
        <h3 class="sk-card-title">
          Pounamu
        </h3>
        <p class="sk-card-subtitle">
          A puzzle game carved out of one rock.
        </p>
      </div>
    </div>
  </div>
</div>

Fixed columns and spans

sk-col-full is 1 / -1, so the banner cell stays full width whatever the column count is.

sk-col-full
sk-col-span-2
1
2
3
4
Show markup
<div class="sk-grid sk-grid-cols-3 sk-gap-3">
  <div class="sink-grid-cell sk-col-full">
    sk-col-full
  </div>
  <div class="sink-grid-cell sk-col-span-2">
    sk-col-span-2
  </div>
  <div class="sink-grid-cell">
    1
  </div>
  <div class="sink-grid-cell">
    2
  </div>
  <div class="sink-grid-cell">
    3
  </div>
  <div class="sink-grid-cell">
    4
  </div>
</div>

Flex utilities

The common arrangements, written as utilities.

sk-flex sk-gap-2
one
two
three
sk-flex sk-gap-2 sk-justify-center
one
two
three
sk-flex sk-gap-2 sk-justify-between
one
two
three
sk-flex sk-gap-2 sk-flex-row-reverse
one
two
three
sk-flex sk-flex-col sk-gap-2
one
two
three
sk-items-* with mixed heights
start
tall
center
tall
end
tall
baseline
tall
stretch
tall
Show markup
<div class="sk-flex sk-flex-col sk-gap-5">
  <div class="sk-flex sk-flex-col sk-gap-1">
    <code class="sink-caption">
      sk-flex sk-gap-2
    </code>
    <div class="sk-flex sk-gap-2">
      <div class="sink-demo-box">
        one
      </div>
      <div class="sink-demo-box">
        two
      </div>
      <div class="sink-demo-box">
        three
      </div>
    </div>
  </div>
  <div class="sk-flex sk-flex-col sk-gap-1">
    <code class="sink-caption">
      sk-flex sk-gap-2 sk-justify-center
    </code>
    <div class="sk-flex sk-gap-2 sk-justify-center">
      <div class="sink-demo-box">
        one
      </div>
      <div class="sink-demo-box">
        two
      </div>
      <div class="sink-demo-box">
        three
      </div>
    </div>
  </div>
  <div class="sk-flex sk-flex-col sk-gap-1">
    <code class="sink-caption">
      sk-flex sk-gap-2 sk-justify-between
    </code>
    <div class="sk-flex sk-gap-2 sk-justify-between">
      <div class="sink-demo-box">
        one
      </div>
      <div class="sink-demo-box">
        two
      </div>
      <div class="sink-demo-box">
        three
      </div>
    </div>
  </div>
  <div class="sk-flex sk-flex-col sk-gap-1">
    <code class="sink-caption">
      sk-flex sk-gap-2 sk-flex-row-reverse
    </code>
    <div class="sk-flex sk-gap-2 sk-flex-row-reverse">
      <div class="sink-demo-box">
        one
      </div>
      <div class="sink-demo-box">
        two
      </div>
      <div class="sink-demo-box">
        three
      </div>
    </div>
  </div>
  <div class="sk-flex sk-flex-col sk-gap-1">
    <code class="sink-caption">
      sk-flex sk-flex-col sk-gap-2
    </code>
    <div class="sk-flex sk-flex-col sk-gap-2">
      <div class="sink-demo-box">
        one
      </div>
      <div class="sink-demo-box">
        two
      </div>
      <div class="sink-demo-box">
        three
      </div>
    </div>
  </div>
  <div class="sk-flex sk-flex-col sk-gap-1">
    <code class="sink-caption">
      sk-items-* with mixed heights
    </code>
    <div class="sk-flex sk-flex-wrap sk-gap-4">
      <div class="sk-flex sk-gap-2 sink-align-demo sk-items-start">
        <div class="sink-demo-box">
          start
        </div>
        <div class="sink-demo-box sink-demo-tall">
          tall
        </div>
      </div>
      <div class="sk-flex sk-gap-2 sink-align-demo sk-items-center">
        <div class="sink-demo-box">
          center
        </div>
        <div class="sink-demo-box sink-demo-tall">
          tall
        </div>
      </div>
      <div class="sk-flex sk-gap-2 sink-align-demo sk-items-end">
        <div class="sink-demo-box">
          end
        </div>
        <div class="sink-demo-box sink-demo-tall">
          tall
        </div>
      </div>
      <div class="sk-flex sk-gap-2 sink-align-demo sk-items-baseline">
        <div class="sink-demo-box">
          baseline
        </div>
        <div class="sink-demo-box sink-demo-tall">
          tall
        </div>
      </div>
      <div class="sk-flex sk-gap-2 sink-align-demo sk-items-stretch">
        <div class="sink-demo-box">
          stretch
        </div>
        <div class="sink-demo-box sink-demo-tall">
          tall
        </div>
      </div>
    </div>
  </div>
</div>

Aligning a control row

The case worth knowing. A field is a column of label plus control, so a plain row containing one is as tall as both; stretch and centre alignment then put the button somewhere you did not mean. sk-items-end fixes it. sk-input-group avoids the question entirely by putting the button inside the field, below the label, where it is aligned by construction.

sk-flex sk-gap-3 - the row is as tall as the field, so the button stretches
sk-flex sk-items-end sk-gap-3 - correct, if you remember it
sk-input-group - correct by construction, nothing to remember
Show markup
<div class="sk-flex sk-flex-col sk-gap-6">
  <div class="sk-flex sk-flex-col sk-gap-2">
    <code class="sink-caption">
      sk-flex sk-gap-3 - the row is as tall as the field, so the button stretches
    </code>
    <div class="sk-flex sk-gap-3">
      <div class="sk-field">
        <label class="sk-label" for="align-a">
          Email
        </label>
        <input class="sk-input" id="align-a" name="align-a" type="email" value="" placeholder="you@example.com" data-size="md" aria-invalid="false">
      </div>
      <button class="sk-button" type="button" data-variant="solid" data-size="md">
        Subscribe
      </button>
    </div>
  </div>
  <div class="sk-flex sk-flex-col sk-gap-2">
    <code class="sink-caption">
      sk-flex sk-items-end sk-gap-3 - correct, if you remember it
    </code>
    <div class="sk-flex sk-items-end sk-gap-3">
      <div class="sk-field">
        <label class="sk-label" for="align-b">
          Email
        </label>
        <input class="sk-input" id="align-b" name="align-b" type="email" value="" placeholder="you@example.com" data-size="md" aria-invalid="false">
      </div>
      <button class="sk-button" type="button" data-variant="solid" data-size="md">
        Subscribe
      </button>
    </div>
  </div>
  <div class="sk-flex sk-flex-col sk-gap-2">
    <code class="sink-caption">
      sk-input-group - correct by construction, nothing to remember
    </code>
    <div class="sk-field">
      <label class="sk-label" for="signup">
        Email
      </label>
      <div class="sk-input-group">
        <input class="sk-input" id="signup" name="signup" type="email" value="" placeholder="you@example.com" data-size="md" aria-invalid="false">
        <button class="sk-button" type="button" data-variant="solid" data-size="md">
          Subscribe
        </button>
      </div>
    </div>
  </div>
</div>

Grow and shrink

sk-min-w-0 is the one people forget: a flex item will not shrink below its content width without it, so one long unbroken string blows the whole row out.

sk-flex-1
auto
auto
sk-grow
auto
sk-min-w-0 stops this blowing the row out: averyveryverylongunbrokenstringindeed
sk-shrink-0
Show markup
<div class="sk-flex sk-flex-col sk-gap-4">
  <div class="sk-flex sk-gap-2">
    <div class="sink-demo-box sk-flex-1">
      sk-flex-1
    </div>
    <div class="sink-demo-box">
      auto
    </div>
  </div>
  <div class="sk-flex sk-gap-2">
    <div class="sink-demo-box">
      auto
    </div>
    <div class="sink-demo-box sk-grow">
      sk-grow
    </div>
    <div class="sink-demo-box">
      auto
    </div>
  </div>
  <div class="sk-flex sk-gap-2">
    <div class="sink-demo-box sk-flex-1 sk-min-w-0 sink-truncate">
      sk-min-w-0 stops this blowing the row out: averyveryverylongunbrokenstringindeed
    </div>
    <div class="sink-demo-box sk-shrink-0">
      sk-shrink-0
    </div>
  </div>
</div>

Stack

Gap is set with data-gap rather than an inline style, so the same markup works unchanged from templ, Svelte or hand-written HTML - and no binding has to emit a style attribute that a strict CSP would block.

data-gap=1
data-gap=3
data-gap=5
Show markup
<div class="sk-stack" data-gap="4">
  <div class="sk-stack" data-gap="2">
    <code class="sink-row-label">
      data-gap=1
    </code>
    <div class="sk-stack" data-gap="1">
      <div class="sink-bar" data-token="space-8">
      </div>
      <div class="sink-bar" data-token="space-8">
      </div>
      <div class="sink-bar" data-token="space-8">
      </div>
    </div>
  </div>
  <div class="sk-stack" data-gap="2">
    <code class="sink-row-label">
      data-gap=3
    </code>
    <div class="sk-stack" data-gap="3">
      <div class="sink-bar" data-token="space-8">
      </div>
      <div class="sink-bar" data-token="space-8">
      </div>
      <div class="sink-bar" data-token="space-8">
      </div>
    </div>
  </div>
  <div class="sk-stack" data-gap="2">
    <code class="sink-row-label">
      data-gap=5
    </code>
    <div class="sk-stack" data-gap="5">
      <div class="sink-bar" data-token="space-8">
      </div>
      <div class="sink-bar" data-token="space-8">
      </div>
      <div class="sink-bar" data-token="space-8">
      </div>
    </div>
  </div>
</div>

Cluster

Wraps by default. Drag the preview narrower to watch it reflow.

Show markup
<div class="sk-cluster" data-gap="2">
  <button class="sk-button" type="button" data-variant="neutral" data-size="sm">
    One
  </button>
  <button class="sk-button" type="button" data-variant="neutral" data-size="sm">
    Two
  </button>
  <button class="sk-button" type="button" data-variant="neutral" data-size="sm">
    Three
  </button>
  <button class="sk-button" type="button" data-variant="neutral" data-size="sm">
    Four
  </button>
  <button class="sk-button" type="button" data-variant="neutral" data-size="sm">
    Five
  </button>
  <button class="sk-button" type="button" data-variant="neutral" data-size="sm">
    A longer label
  </button>
  <button class="sk-button" type="button" data-variant="neutral" data-size="sm">
    And another one
  </button>
</div>

Container widths

data-width=sm
data-width=md
data-width=lg
data-width=xl
Show markup
<div class="sk-stack" data-gap="3">
  <div class="sk-container" data-width="sm">
    <div class="sink-demo-box">
      <code>
        data-width=sm
      </code>
    </div>
  </div>
  <div class="sk-container" data-width="md">
    <div class="sink-demo-box">
      <code>
        data-width=md
      </code>
    </div>
  </div>
  <div class="sk-container" data-width="lg">
    <div class="sink-demo-box">
      <code>
        data-width=lg
      </code>
    </div>
  </div>
  <div class="sk-container" data-width="xl">
    <div class="sink-demo-box">
      <code>
        data-width=xl
      </code>
    </div>
  </div>
</div>

Cascade layers

Everything in sk.css lives inside @layer. The kitchen sink's own stylesheet is unlayered, so it wins automatically - no !important anywhere in the repository. That is the property that makes the bundle safe to drop into an app that already has CSS.

The stylesheet declares its order up front:

@layer sk.reset, sk.tokens, sk.base, sk.components, sk.utilities;

Unlayered rules in a host application beat every one of those layers regardless of specificity, so overriding a component never needs an escalation war. The sidebar and preview frames around this page are styled exactly that way.

Show markup
<div>
  <p>
    The stylesheet declares its order up front:
  </p>
  <pre>
    <code>
      @layer sk.reset, sk.tokens, sk.base, sk.components, sk.utilities;
    </code>
  </pre>
  <p>
    Unlayered rules in a host application beat every one of those layers regardless of specificity, so overriding a component never needs an escalation war. The sidebar and preview frames around this page are styled exactly that way.
  </p>
</div>