<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[oks-ui]]></title><description><![CDATA[Strict TypeScript React components. Zero runtime dependencies, CSS-variable theming, accessible by default. MIT. www.oks-ui.com]]></description><link>https://oks-ui.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ab76ae287f2acbed7e02ff1/f6d75104-ce5b-44ec-bbed-6d9cf286c571.png</url><title>oks-ui</title><link>https://oks-ui.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 10 Oct 2026 11:11:04 GMT</lastBuildDate><atom:link href="https://oks-ui.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Dark mode with no flash: the data-theme init pattern]]></title><description><![CDATA[The classic dark-mode bug: a user with dark mode saved loads your page, and for one visible frame it's light — then it snaps to dark. The fix isn't complicated, but it only works if you do it in a ver]]></description><link>https://oks-ui.hashnode.dev/dark-mode-with-no-flash-the-data-theme-init-pattern</link><guid isPermaLink="true">https://oks-ui.hashnode.dev/dark-mode-with-no-flash-the-data-theme-init-pattern</guid><dc:creator><![CDATA[omkar sahu]]></dc:creator><pubDate>Wed, 07 Oct 2026 05:43:35 GMT</pubDate><content:encoded><![CDATA[<p>The classic dark-mode bug: a user with dark mode saved loads your page, and for one visible frame it's light — then it snaps to dark. The fix isn't complicated, but it only works if you do it in a very specific order, and most "just add a class in <code>useEffect</code>" implementations get that order wrong. Here's the pattern this site itself runs on, in full.</p>
<h2>Why the flash happens at all</h2>
<p>Server-rendered HTML has no idea what theme a returning visitor prefers — that preference lives in the browser's <code>localStorage</code>, which the server can't read. So the page necessarily ships with some default theme baked into the markup. If you apply the real theme in a React <code>useEffect</code>, that effect runs <em>after</em> the browser has already painted the default — hence the flash.</p>
<h2>The fix: a synchronous script before hydration</h2>
<p>The theme has to be applied before the browser paints anything, which means before React even loads. A plain, tiny, synchronous <code>&lt;script&gt;</code> in <code>&lt;head&gt;</code> — or as early in <code>&lt;body&gt;</code> as your framework allows — does exactly that:</p>
<pre><code class="language-js">(function () {
  try {
    var stored = localStorage.getItem("oks-ui-theme");
    var theme = stored === "light" || stored === "dark"
      ? stored
      : (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
    document.documentElement.setAttribute("data-theme", theme);
  } catch (e) {}
})();
</code></pre>
<p>Three things make this work that are easy to get wrong in a rewrite:</p>
<ul>
<li><p><strong>It runs before hydration, not in an effect.</strong> In Next.js that's <code>&lt;Script strategy="beforeInteractive"&gt;</code>; in a non-Next app it's a plain inline <code>&lt;script&gt;</code> placed before your stylesheet, since the attribute has to exist before the CSS that reads it is applied.</p>
</li>
<li><p><strong>It falls back through three sources in order</strong>: an explicit stored choice, then the OS-level <code>prefers-color-scheme</code> media query, then a hard default. Skipping the media-query fallback means a first-time visitor with dark mode set at the OS level still sees a light flash.</p>
</li>
<li><p><strong>It's wrapped in</strong> <code>try</code><strong>/</strong><code>catch</code><strong>.</strong> <code>localStorage</code> throws in some locked-down or privacy-mode browser contexts — a script this early in the page load has no error boundary to catch it, so an uncaught throw here can block the rest of the page from rendering at all.</p>
</li>
</ul>
<h2>The React side: expect the mismatch, don't fight it</h2>
<p>The script sets <code>data-theme</code> on <code>&lt;html&gt;</code> directly, outside React's tree — which means React's hydration will notice that attribute the server never rendered and, by default, log a mismatch warning. The fix isn't to move the logic into React, which reintroduces the flash — it's to tell React this one attribute is expected to differ:</p>
<pre><code class="language-tsx">&lt;html lang="en" suppressHydrationWarning&gt;
  {/* ... */}
&lt;/html&gt;
</code></pre>
<blockquote>
<p><strong>Scope</strong> <code>suppressHydrationWarning</code> <strong>narrowly.</strong> Put it only on the element whose attributes the script actually touches — <code>&lt;html&gt;</code> here. Slapping it on a much larger subtree silences real hydration bugs along with the expected one.</p>
</blockquote>
<h2>Everything downstream is just CSS</h2>
<p>Once <code>data-theme="dark"</code> is on <code>&lt;html&gt;</code>, oks-ui's tokens respond to it with an ordinary attribute selector — no React context, no re-render, no provider:</p>
<pre><code class="language-css">:root { --oks-color-surface: #fff; }
:root[data-theme="dark"] { --oks-color-surface: #0a0a0a; }
</code></pre>
<p>A toggle button just flips the attribute and writes the same key back to <code>localStorage</code> — no re-run of the init script needed, since the browser already has the page open. See the full token list this responds to at <a href="/docs/theming/dark-mode/">Dark mode</a>.</p>
<hr />
<p><em>Originally published at</em> <a href="https://www.oks-ui.com/blog/dark-mode-with-no-flash/"><em>oks-ui.com</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Build an Admin Dashboard in React]]></title><description><![CDATA[An admin dashboard is mostly three things: a row of numbers that matter, a chart that shows the trend, and a table of recent activity. In this tutorial we build all three with oks-ui and arrange them ]]></description><link>https://oks-ui.hashnode.dev/build-an-admin-dashboard-in-react</link><guid isPermaLink="true">https://oks-ui.hashnode.dev/build-an-admin-dashboard-in-react</guid><dc:creator><![CDATA[omkar sahu]]></dc:creator><pubDate>Wed, 07 Oct 2026 05:40:26 GMT</pubDate><content:encoded><![CDATA[<p>An admin dashboard is mostly three things: a row of numbers that matter, a chart that shows the trend, and a table of recent activity. In this tutorial we build all three with oks-ui and arrange them in a layout that works from phone to desktop.</p>
<pre><code class="language-tsx">import { Card } from "oks-ui/card";
import "oks-ui/card.css";
import { Stat } from "oks-ui/stat";
import "oks-ui/stat.css";
import { Chart } from "oks-ui/chart";
import "oks-ui/chart.css";
import { Table, type TableColumn } from "oks-ui/table";
import "oks-ui/table.css";
import { Chip } from "oks-ui/chip";
import "oks-ui/chip.css";
</code></pre>
<h2>Decide what goes on it first</h2>
<p>The hardest part of a dashboard isn't the code, it's choosing what to show. A useful test: every number on the first screen should answer a question someone actually asks every day. "How much did we sell this week?" earns a card. "Total registered users since launch" usually doesn't, because it only ever goes up and nobody acts on it. Keep the top row to four numbers. Show each one next to its change over a comparable period, because a number without context can't tell you whether today is good or bad. Put detail — the chart and the table — underneath, where people look once the top row has told them something is worth a closer look.</p>
<h2>1. KPI cards</h2>
<p><code>Stat</code> shows a label, a big value, and an optional change with a direction. The direction colours the change pill and picks the arrow.</p>
<pre><code class="language-tsx">const kpis = [
  { label: "Revenue", value: "$48,210", delta: "+12.4%", trend: "up" },
  { label: "Orders", value: "1,284", delta: "+3.1%", trend: "up" },
  { label: "Refunds", value: "18", delta: "-2", trend: "down" },
  { label: "Conversion", value: "3.2%", delta: "0.0%", trend: "flat" },
] as const;

&lt;div className="kpi-grid"&gt;
  {kpis.map((k) =&gt; (
    &lt;Card key={k.label} shadow="sm" radius="lg" style={{ padding: 20 }}&gt;
      &lt;Stat label={k.label} value={k.value} delta={k.delta} trend={k.trend} /&gt;
    &lt;/Card&gt;
  ))}
&lt;/div&gt;
</code></pre>
<p><code>Stat</code> also takes an <code>icon</code>, a <code>help</code> line under the value, and a <code>spark</code> slot for a tiny chart.</p>
<h2>2. The revenue chart</h2>
<p><code>Chart</code> takes rows of data, the key for the x-axis, and the series to draw. Multiple series get a legend automatically.</p>
<pre><code class="language-tsx">const revenue = [
  { month: "Jan", online: 12400, store: 8200 },
  { month: "Feb", online: 13900, store: 7900 },
  { month: "Mar", online: 15100, store: 8800 },
  { month: "Apr", online: 16800, store: 9100 },
  { month: "May", online: 18200, store: 9600 },
  { month: "Jun", online: 21000, store: 10400 },
];

&lt;Card shadow="sm" radius="lg" style={{ padding: 20 }}&gt;
  &lt;Chart
    type="area"
    title="Revenue"
    description="Online vs. in-store, last 6 months"
    data={revenue}
    x="month"
    series={[
      { key: "online", name: "Online" },
      { key: "store", name: "In store" },
    ]}
    height={280}
  /&gt;
&lt;/Card&gt;
</code></pre>
<p>Change <code>type</code> to <code>"line"</code> or <code>"column"</code> and nothing else needs to change. The chart has tooltips on hover and can be browsed with the arrow keys.</p>
<h2>3. Recent orders</h2>
<p><code>Table</code> takes column definitions and rows. A column's <code>render</code> function controls how a cell looks — here, a status <code>Chip</code>.</p>
<pre><code class="language-tsx">type Order = { id: string; customer: string; total: number; status: "Paid" | "Pending" | "Refunded" };

const STATUS_COLOR = { Paid: "success", Pending: "warning", Refunded: "default" } as const;

const columns: TableColumn&lt;Order&gt;[] = [
  { key: "id", header: "Order" },
  { key: "customer", header: "Customer", sortable: true },
  {
    key: "total",
    header: "Total",
    align: "end",
    sortable: true,
    render: (o) =&gt; `$${o.total.toFixed(2)}`,
  },
  {
    key: "status",
    header: "Status",
    render: (o) =&gt; (
      &lt;Chip size="sm" variant="soft" color={STATUS_COLOR[o.status]}&gt;{o.status}&lt;/Chip&gt;
    ),
  },
];

&lt;Card shadow="sm" radius="lg" style={{ padding: 20 }}&gt;
  &lt;Table aria-label="Recent orders" columns={columns} rows={orders} getRowKey={(o) =&gt; o.id} /&gt;
&lt;/Card&gt;
</code></pre>
<p>Sortable columns sort on click, with no extra code.</p>
<h2>4. The layout</h2>
<p>A few lines of CSS grid make it responsive: four KPI cards in a row on desktop, two on phones.</p>
<pre><code class="language-css">.dashboard { display: grid; gap: 16px; }
.kpi-grid { display: grid; gap: 16px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.dashboard-main { display: grid; gap: 16px; grid-template-columns: 2fr 1fr; }

@media (max-width: 900px) {
  .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dashboard-main { grid-template-columns: 1fr; }
}
</code></pre>
<p>Put the chart in the wider column of <code>.dashboard-main</code> and a smaller card — top products, or a donut chart — in the narrow one.</p>
<h2>5. Loading states</h2>
<p>Dashboards load data, and blank cards while waiting look broken. <code>Table</code> shows skeleton rows with <code>isLoading</code>, and <code>Chart</code> shows a loading overlay with the same prop:</p>
<pre><code class="language-tsx">&lt;Table aria-label="Recent orders" columns={columns} rows={orders ?? []} getRowKey={(o) =&gt; o.id} isLoading={!orders} /&gt;
&lt;Chart type="area" data={revenue ?? []} x="month" series="online" isLoading={!revenue} /&gt;
</code></pre>
<h2>Dark mode</h2>
<p>Every component here uses oks-ui's design tokens, so the whole dashboard switches to dark mode when you set <code>data-theme="dark"</code> on the <code>html</code> element — cards, chart, table and chips included. See a complete version on the <a href="/patterns/analytics-dashboard/">patterns page</a>.</p>
<hr />
<p><em>Originally published at</em> <a href="https://www.oks-ui.com/blog/build-admin-dashboard-in-react/"><em>oks-ui.com</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Reduce React Bundle Size with Per-Component Imports]]></title><description><![CDATA[A component library should cost you only the components you use. In practice, importing everything from a library's main entry point often ships far more than that. This guide explains why, and how to]]></description><link>https://oks-ui.hashnode.dev/reduce-react-bundle-size-with-per-component-imports</link><guid isPermaLink="true">https://oks-ui.hashnode.dev/reduce-react-bundle-size-with-per-component-imports</guid><dc:creator><![CDATA[omkar sahu]]></dc:creator><pubDate>Wed, 07 Oct 2026 05:36:17 GMT</pubDate><content:encoded><![CDATA[<p>A component library should cost you only the components you use. In practice, importing everything from a library's main entry point often ships far more than that. This guide explains why, and how to import oks-ui so your app pays only for what it renders.</p>
<h2>Why "just import it" can be expensive</h2>
<p>The usual import looks harmless:</p>
<pre><code class="language-tsx">import { Button, Modal, Table } from "oks-ui";
import "oks-ui/styles.css";
</code></pre>
<p>Bundlers try to drop unused exports ("tree shaking"), but they can only drop code they can prove has no side effects. A stylesheet import is a side effect by definition, and a single combined stylesheet contains every component's CSS no matter which ones you use. The result in many real apps: the whole library ships. For oks-ui that is around 770 KB of JavaScript before compression and a 320 KB stylesheet — even for a page with three components.</p>
<h2>Import each component from its own path</h2>
<p>Since version 1.2, every oks-ui component has its own entry point and its own stylesheet:</p>
<pre><code class="language-tsx">import { Button } from "oks-ui/button";
import "oks-ui/button.css";

import { Modal } from "oks-ui/modal";
import "oks-ui/modal.css";

import { Table } from "oks-ui/table";
import "oks-ui/table.css";
</code></pre>
<p>The bundler has nothing to guess about: it includes the files you imported and nothing else. Two things make this easy:</p>
<ul>
<li><p><strong>Stylesheets include their parts.</strong> <code>oks-ui/modal.css</code> already contains the Backdrop's styles, and <code>oks-ui/table.css</code> covers its empty state and loading skeleton. You never need to know what a component is made of.</p>
</li>
<li><p><strong>Tokens stay global.</strong> Load <code>oks-ui/tokens.css</code> (and optionally <code>oks-ui/utilities.css</code>) once at the root of your app. Every component reads the same colour, spacing and radius variables. The path is the component name in kebab-case: <code>oks-ui/button-group</code>, <code>oks-ui/page-title</code>, <code>oks-ui/form-field-set</code>, <code>oks-ui/text-editor</code>.</p>
</li>
</ul>
<h2>Next.js and Turbopack</h2>
<p>Each component's JavaScript also imports its own stylesheet, and most bundlers — webpack, Vite, Rollup, Rspack — follow that automatically. Next.js with Turbopack does not follow a stylesheet imported from inside a package, so write the <code>.css</code> import yourself. It's harmless elsewhere, so the simplest rule is to always write it. In the App Router, add <code>"use client"</code> to files that render oks-ui components. A small wrapper file per component keeps that boundary contained, so your pages can stay server components.</p>
<h2>Migration checklist</h2>
<ol>
<li><p>Replace <code>import "oks-ui/styles.css"</code> with <code>oks-ui/tokens.css</code> and <code>oks-ui/utilities.css</code> in your root stylesheet.</p>
</li>
<li><p>Change each <code>from "oks-ui"</code> import to the component's own path, and add its <code>.css</code> import beside it.</p>
</li>
<li><p>Search for any remaining <code>from "oks-ui"</code> — one leftover import can pull the whole library back in.</p>
</li>
<li><p>Open each page and look for unstyled components. A missing <code>.css</code> import is the only thing that can go wrong, and it is obvious on sight. Old and new imports work side by side, so you can migrate one file at a time.</p>
</li>
</ol>
<h2>Measure it</h2>
<p>Don't trust the theory — measure before and after:</p>
<ul>
<li><p>Your framework's build output lists the size of each route.</p>
</li>
<li><p>A bundle analyzer shows which packages end up in each chunk.</p>
</li>
<li><p>Lighthouse on a throttled mobile profile shows what the difference means for real users. On the oks-ui website, moving to per-component imports cut compressed CSS from 48 KB to 16 KB and JavaScript by about a fifth. Less CSS matters most on phones, because the browser can't paint the page until its stylesheets have arrived. See both import styles in the <a href="/docs/installation/">installation guide</a>.</p>
</li>
</ul>
<hr />
<p><em>Originally published at</em> <a href="https://www.oks-ui.com/blog/reduce-react-bundle-size-per-component-imports/"><em>oks-ui.com</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Build a sortable, selectable data table]]></title><description><![CDATA[Search, sort, select, paginate — almost every admin screen needs the same data table, and almost every team ends up hand-rolling it because no single component quite covers all four. Here's the whole ]]></description><link>https://oks-ui.hashnode.dev/build-a-sortable-selectable-data-table</link><guid isPermaLink="true">https://oks-ui.hashnode.dev/build-a-sortable-selectable-data-table</guid><dc:creator><![CDATA[omkar sahu]]></dc:creator><pubDate>Tue, 06 Oct 2026 03:19:22 GMT</pubDate><content:encoded><![CDATA[<p>Search, sort, select, paginate — almost every admin screen needs the same data table, and almost every team ends up hand-rolling it because no single component quite covers all four. Here's the whole thing with <code>Table</code>, <code>Pagination</code> and <code>FormFieldSet</code>, wired together with plain React state.</p>
<h2>1. The shape of the data</h2>
<p><code>Table</code> takes rows as plain objects and a <code>getRowKey</code> function — no adapter layer, no special row type.</p>
<pre><code class="language-tsx">type Row = { id: number; name: string; email: string; role: string };

const DATA: Row[] = [
  { id: 1, name: "Ada Lovelace", email: "ada@oks-ui.com", role: "Owner" },
  { id: 2, name: "Grace Hopper", email: "grace@oks-ui.com", role: "Admin" },
  { id: 3, name: "Alan Turing", email: "alan@oks-ui.com", role: "Member" },
  // ...
];
</code></pre>
<h2>2. Search filters the rows, not the table</h2>
<p><code>Table</code> itself has no search box — it just renders whatever <code>rows</code> you give it. Filtering lives in your own state, one <code>useMemo</code>:</p>
<pre><code class="language-tsx">const [query, setQuery] = useState("");
const [page, setPage] = useState(1);

const filtered = useMemo(
  () =&gt; DATA.filter((r) =&gt; r.name.toLowerCase().includes(query.toLowerCase())),
  [query],
);
</code></pre>
<h2>3. Sorting, selection, and the table itself</h2>
<p>Pass <code>sortable: true</code> on a column and <code>Table</code> handles the click-to-sort interface; omit <code>onSortChange</code> and it sorts client-side automatically — supply it and you get the descriptor back instead, for a server-side sort. <code>selectionMode="multiple"</code> plus <code>selectedKeys</code> and <code>onSelectionChange</code> gets you checkboxes and an indeterminate header state for free:</p>
<pre><code class="language-tsx">const [selected, setSelected] = useState&lt;Set&lt;number&gt;&gt;(new Set());

&lt;Table
  aria-label="Team members"
  columns={[
    { key: "name", header: "Name", sortable: true },
    { key: "email", header: "Email" },
    { key: "role", header: "Role" },
  ]}
  rows={pageRows}
  getRowKey={(r) =&gt; r.id}
  selectionMode="multiple"
  selectedKeys={selected}
  onSelectionChange={(keys) =&gt; setSelected(keys as Set&lt;number&gt;)}
/&gt;
</code></pre>
<blockquote>
<p><code>aria-label</code> <strong>is required, not optional.</strong> <code>Table</code> throws in development if you skip it — a data table with no accessible name is unusable with a screen reader, so the component won't let you forget it.</p>
</blockquote>
<h2>4. Pagination, wired to the same filtered list</h2>
<p><code>Pagination</code> and <code>PaginationSummary</code> are just told the current page, the page count and the total — they don't know or care that the underlying data came from a search filter:</p>
<pre><code class="language-tsx">const PAGE_SIZE = 4;
const pageRows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

&lt;PaginationSummary page={page} pageSize={PAGE_SIZE} total={filtered.length} /&gt;
&lt;Pagination
  page={page}
  pageCount={Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))}
  onChange={setPage}
/&gt;
</code></pre>
<h2>Loading, empty and expanded states</h2>
<p>Three props round out the states a real table needs: <code>isLoading</code> swaps in skeleton rows, <code>emptyContent</code> replaces the body when <code>rows</code> is empty (it already defaults to a sensible empty state), and <code>renderExpandedRow</code> adds a chevron-toggle column for a details panel under any row — no layout work required for any of the three. Full prop reference at <a href="/components/table/">the Table component page</a>, and this exact example — search, sort, select, paginate, wired end to end — as a live preview with the complete source at <a href="/patterns/data-table-view/">the data table pattern</a>.</p>
<hr />
<p><em>Originally published at</em> <a href="https://www.oks-ui.com/blog/build-a-sortable-selectable-data-table/"><em>oks-ui.com</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Theme oks-ui to your brand in 10 minutes]]></title><description><![CDATA[oks-ui ships with a neutral default look on purpose — every colour, radius, space and shadow it renders is a plain CSS custom property, not a baked-in design opinion. That means re-theming it isn't a ]]></description><link>https://oks-ui.hashnode.dev/theme-oks-ui-to-your-brand-in-10-minutes</link><guid isPermaLink="true">https://oks-ui.hashnode.dev/theme-oks-ui-to-your-brand-in-10-minutes</guid><dc:creator><![CDATA[omkar sahu]]></dc:creator><pubDate>Tue, 06 Oct 2026 03:11:31 GMT</pubDate><content:encoded><![CDATA[<p>oks-ui ships with a neutral default look on purpose — every colour, radius, space and shadow it renders is a plain CSS custom property, not a baked-in design opinion. That means re-theming it isn't a config file or a build step; it's a stylesheet. Here's the whole path from default to your brand, in the order that actually matters.</p>
<h2>1. Start with the primary ramp, not just one colour</h2>
<p><code>primary</code> is the semantic role that colours your main buttons, links and focus rings. It's tempting to override just <code>--oks-color-primary-500</code> and call it done, but a solid button also reads <code>600</code> for hover and <code>700</code> for pressed — override the whole ramp so those states stay coherent instead of falling back to the default orange.</p>
<pre><code class="language-css">:root {
  --oks-color-primary-50:  #eef2ff;
  --oks-color-primary-100: #e0e7ff;
  --oks-color-primary-500: #4f46e5;  /* your brand */
  --oks-color-primary-600: #4338ca;  /* hover */
  --oks-color-primary-700: #3730a3;  /* active / pressed */
}
</code></pre>
<blockquote>
<p><strong>Check contrast before you ship it.</strong> A solid Button or Chip puts white label text on <code>500</code> and deeper by default. Before you lock in a brand colour, check that white clears <strong>4.5:1</strong> against it — a vivid, light colour like a saturated yellow or orange usually won't, and needs a slightly deeper 500 to stay accessible.</p>
</blockquote>
<h2>2. Match your shape language</h2>
<p>Radius is one scale, reused everywhere — cards, buttons, inputs, modals. Nudge the two or three sizes you actually see and the whole interface shifts from sharp to soft in one edit.</p>
<pre><code class="language-css">:root {
  --oks-radius-sm: 0.125rem;  /* chips, small controls */
  --oks-radius-md: 0.5rem;    /* the one most components default to */
  --oks-radius-lg: 0.875rem;  /* cards, modals */
}
</code></pre>
<h2>3. Dial in density</h2>
<p>The space scale drives padding and gaps across the library. If your product wants to feel airier or more compact than the default, nudge <code>--oks-space-4</code> — it's the workhorse most components reach for.</p>
<pre><code class="language-css">:root { --oks-space-4: 14px; }  /* default is 16px */
</code></pre>
<h2>4. Point neutral surfaces at your own grey</h2>
<p>If your brand's grey isn't oks-ui's default, alias the <code>neutral</code> role instead of overriding a dozen individual tokens — form-field borders and muted text all read from it.</p>
<pre><code class="language-css">:root {
  --oks-color-neutral-100: var(--oks-palette-stone-100);
  --oks-color-neutral-500: var(--oks-palette-stone-500);
}
</code></pre>
<h2>5. Do it again for dark mode</h2>
<p>Dark mode is a <code>data-theme="dark"</code> attribute on <code>&lt;html&gt;</code>, not a second theme system — repeat the same overrides under that selector. The one thing to remember is that two tokens, <code>--oks-color-surface</code> and the form-field background, have light-only defaults, so your app has to supply the dark values itself:</p>
<pre><code class="language-css">:root[data-theme="dark"] {
  --oks-color-primary-500: #6366f1;  /* usually a touch lighter than light mode */
  --oks-color-surface: #0a0a0a;
  --oks-form-field-bg: #171717;
}
</code></pre>
<p>That's the whole system — no provider, no rebuild, no JavaScript theme object. See the full token reference at <a href="/docs/theming/">Theming</a> and dark-mode specifics at <a href="/docs/theming/dark-mode/">Dark mode</a>.</p>
<hr />
<p><em>Originally published at <a href="https://www.oks-ui.com/blog/theme-oks-ui-to-your-brand-in-10-minutes/">oks-ui.com</a>.</em></p>
]]></content:encoded></item></channel></rss>