Pure CSSMIT licensed

Textured Button and Input Surfaces

A complete form kit where every interactive surface is a material: buttons, inputs, a select, checkboxes, a range slider and a segmented control, all carrying the same grain token at weights tuned per state. Every control is at least 44px, every focus ring is visible, and pressing a button genuinely feels like pressing something.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: a 10,000+ component Figma UI kit · from $129 one-time

Untitled UI · Affiliate link

Get Untitled UI Pro

The code

<section class="grn-12" aria-labelledby="grn-12-title">
  <div class="grn-12__theme">
    <input class="grn-12__theme-input" type="checkbox" id="grn-12-theme">
    <label class="grn-12__theme-label" for="grn-12-theme">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="grn-12__sr">Toggle dark theme</span>
    </label>
  </div>

  <main class="grn-12__shell">
    <header class="grn-12__head">
      <p class="grn-12__kicker">Control surfaces · 12</p>
      <h1 class="grn-12__h1" id="grn-12-title">Every control is a material</h1>
      <p class="grn-12__lede">Buttons are raised, inputs are recessed, and both carry the same grain. Press a button and three things change at once — grain weight, highlight direction, shadow depth.</p>
    </header>

    <div class="grn-12__cols">
      <section class="grn-12__card" aria-labelledby="grn-12-h-btn">
        <h2 class="grn-12__h2" id="grn-12-h-btn">Buttons</h2>
        <div class="grn-12__btn-row">
          <button class="grn-12__btn" type="button">Save changes</button>
          <button class="grn-12__btn grn-12__btn--sec" type="button">Preview</button>
          <button class="grn-12__btn grn-12__btn--ghost" type="button">Cancel</button>
          <button class="grn-12__btn" type="button" disabled>Disabled</button>
        </div>
        <div class="grn-12__seg" role="group" aria-label="Density">
          <input class="grn-12__seg-in" type="radio" name="grn-12-density" id="grn-12-d1" checked>
          <label class="grn-12__seg-lb" for="grn-12-d1">Compact</label>
          <input class="grn-12__seg-in" type="radio" name="grn-12-density" id="grn-12-d2">
          <label class="grn-12__seg-lb" for="grn-12-d2">Regular</label>
          <input class="grn-12__seg-in" type="radio" name="grn-12-density" id="grn-12-d3">
          <label class="grn-12__seg-lb" for="grn-12-d3">Roomy</label>
        </div>
        <p class="grn-12__hint">Idle 0.05 · hover 0.06 · active 0.08 with the highlight inverted.</p>
      </section>

      <section class="grn-12__card" aria-labelledby="grn-12-h-fields">
        <h2 class="grn-12__h2" id="grn-12-h-fields">Fields</h2>
        <div class="grn-12__field">
          <label class="grn-12__label" for="grn-12-name">Full name</label>
          <input class="grn-12__input" type="text" id="grn-12-name" name="grn-12-name" placeholder="Ada Lovelace" autocomplete="name">
        </div>
        <div class="grn-12__field">
          <label class="grn-12__label" for="grn-12-mail">Work email</label>
          <input class="grn-12__input" type="email" id="grn-12-mail" name="grn-12-mail" placeholder="ada@example.com" autocomplete="email" required aria-describedby="grn-12-mail-hint">
          <p class="grn-12__err" id="grn-12-mail-hint">We only email about deploys. Real address required.</p>
        </div>
        <div class="grn-12__field">
          <label class="grn-12__label" for="grn-12-region">Region</label>
          <select class="grn-12__input grn-12__select" id="grn-12-region" name="grn-12-region">
            <option>eu-west-2 · London</option>
            <option>us-east-1 · Virginia</option>
            <option>ap-southeast-2 · Sydney</option>
          </select>
        </div>
      </section>

      <section class="grn-12__card" aria-labelledby="grn-12-h-misc">
        <h2 class="grn-12__h2" id="grn-12-h-misc">Toggles and ranges</h2>
        <ul class="grn-12__checks">
          <li><input class="grn-12__check" type="checkbox" id="grn-12-c1" checked><label for="grn-12-c1">Textured surfaces</label></li>
          <li><input class="grn-12__check" type="checkbox" id="grn-12-c2"><label for="grn-12-c2">Reduce transparency</label></li>
          <li><input class="grn-12__check" type="radio" name="grn-12-r" id="grn-12-r1" checked><label for="grn-12-r1">Match system theme</label></li>
          <li><input class="grn-12__check" type="radio" name="grn-12-r" id="grn-12-r2"><label for="grn-12-r2">Always dark</label></li>
        </ul>
        <div class="grn-12__field">
          <label class="grn-12__label" for="grn-12-grain">Grain weight</label>
          <input class="grn-12__range" type="range" id="grn-12-grain" name="grn-12-grain" min="0" max="20" value="6" step="1">
          <p class="grn-12__hint">Native thumb, textured track, <code>accent-color</code> for the fill.</p>
        </div>
      </section>
    </div>
  </main>
</section>
.grn-12 {
  --grn-12-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='grn12n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23grn12n)'/%3E%3C/svg%3E");
  --grn-12-bg: #eef0f7;
  --grn-12-surf: #ffffff;
  --grn-12-sunk: #e7eaf4;
  --grn-12-line: #d3d8e8;
  --grn-12-ink: #14162a;
  --grn-12-mut: #5c6280;
  --grn-12-acc: #4f46e5;
  --grn-12-acc: oklch(52% 0.22 275);
  --grn-12-on-acc: #f2f1fe;
  --grn-12-err: #b42318;
  --grn-12-err: oklch(50% 0.18 27);
  --grn-12-g-idle: 0.05;
  --grn-12-g-hover: 0.06;
  --grn-12-g-active: 0.08;
  --grn-12-g-sunk: 0.035;
  --grn-12-blend: multiply;
  --grn-12-hl: rgba(255, 255, 255, 0.85);
  --grn-12-radius: 12px;
  --grn-12-font-display: "Space Grotesk", "Inter Tight", ui-sans-serif, system-ui, sans-serif;
  --grn-12-font-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", Menlo, monospace;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--grn-12-bg);
  color: var(--grn-12-ink);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  line-height: 1.55;
  accent-color: var(--grn-12-acc);
}

.grn-12 *,
.grn-12 *::before,
.grn-12 *::after {
  box-sizing: border-box;
}

.grn-12 :focus-visible {
  outline: 2px solid var(--grn-12-acc);
  outline-offset: 2px;
}

.grn-12 code {
  font-family: var(--grn-12-font-mono);
  font-size: 0.9em;
}

.grn-12__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.grn-12:has(.grn-12__theme-input:checked) {
  --grn-12-bg: #0c0d16;
  --grn-12-surf: #15172a;
  --grn-12-sunk: #10121f;
  --grn-12-line: #262a45;
  --grn-12-ink: #ecebfb;
  --grn-12-mut: #9a9ac4;
  --grn-12-acc: #a5b4fc;
  --grn-12-acc: oklch(80% 0.11 278);
  --grn-12-on-acc: #10122a;
  --grn-12-err: #fca5a5;
  --grn-12-err: oklch(78% 0.13 22);
  --grn-12-blend: soft-light;
  --grn-12-hl: rgba(255, 255, 255, 0.12);
  --grn-12-g-idle: 0.07;
  --grn-12-g-hover: 0.085;
  --grn-12-g-active: 0.11;
  --grn-12-g-sunk: 0.05;
}

@media (prefers-color-scheme: dark) {
  .grn-12 {
    --grn-12-bg: #0c0d16;
    --grn-12-surf: #15172a;
    --grn-12-sunk: #10121f;
    --grn-12-line: #262a45;
    --grn-12-ink: #ecebfb;
    --grn-12-mut: #9a9ac4;
    --grn-12-acc: #a5b4fc;
    --grn-12-acc: oklch(80% 0.11 278);
    --grn-12-on-acc: #10122a;
    --grn-12-err: #fca5a5;
    --grn-12-err: oklch(78% 0.13 22);
    --grn-12-blend: soft-light;
    --grn-12-hl: rgba(255, 255, 255, 0.12);
    --grn-12-g-idle: 0.07;
    --grn-12-g-hover: 0.085;
    --grn-12-g-active: 0.11;
    --grn-12-g-sunk: 0.05;
  }

  .grn-12:has(.grn-12__theme-input:checked) {
    --grn-12-bg: #eef0f7;
    --grn-12-surf: #ffffff;
    --grn-12-sunk: #e7eaf4;
    --grn-12-line: #d3d8e8;
    --grn-12-ink: #14162a;
    --grn-12-mut: #5c6280;
    --grn-12-acc: #4f46e5;
    --grn-12-acc: oklch(52% 0.22 275);
    --grn-12-on-acc: #f2f1fe;
    --grn-12-err: #b42318;
    --grn-12-err: oklch(50% 0.18 27);
    --grn-12-blend: multiply;
    --grn-12-hl: rgba(255, 255, 255, 0.85);
    --grn-12-g-idle: 0.05;
    --grn-12-g-hover: 0.06;
    --grn-12-g-active: 0.08;
    --grn-12-g-sunk: 0.035;
  }
}

@media (prefers-color-scheme: light) {
  .grn-12:has(.grn-12__theme-input:checked) {
    --grn-12-bg: #0c0d16;
    --grn-12-surf: #15172a;
    --grn-12-sunk: #10121f;
    --grn-12-line: #262a45;
    --grn-12-ink: #ecebfb;
    --grn-12-mut: #9a9ac4;
    --grn-12-acc: #a5b4fc;
    --grn-12-acc: oklch(80% 0.11 278);
    --grn-12-on-acc: #10122a;
    --grn-12-err: #fca5a5;
    --grn-12-err: oklch(78% 0.13 22);
    --grn-12-blend: soft-light;
    --grn-12-hl: rgba(255, 255, 255, 0.12);
    --grn-12-g-idle: 0.07;
    --grn-12-g-hover: 0.085;
    --grn-12-g-active: 0.11;
    --grn-12-g-sunk: 0.05;
  }
}

.grn-12__theme {
  position: absolute;
  inset-block-start: clamp(1rem, 3vw, 1.75rem);
  inset-inline-end: clamp(0.85rem, 3vw, 1.75rem);
  z-index: 40;
}

.grn-12__theme-input {
  position: absolute;
  inset: 0;
  opacity: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  cursor: pointer;
}

.grn-12__theme-label {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid var(--grn-12-line);
  border-radius: 999px;
  background: var(--grn-12-surf);
  color: var(--grn-12-ink);
  cursor: pointer;
  box-shadow: inset 0 1px 0 var(--grn-12-hl);
}

.grn-12__theme-label svg {
  inline-size: 20px;
  block-size: 20px;
}

.grn-12__shell {
  inline-size: min(76rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
  margin-inline: auto;
  padding-block: clamp(3rem, 8vw, 5.5rem) clamp(2.5rem, 6vw, 4rem);
  display: grid;
  gap: clamp(2rem, 5vw, 3rem);
}

.grn-12__head {
  max-inline-size: 44rem;
  display: grid;
  gap: 0.6rem;
}

.grn-12__kicker {
  margin: 0;
  font: 500 0.72rem/1 var(--grn-12-font-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--grn-12-acc);
}

.grn-12__h1 {
  margin: 0;
  font: 600 clamp(2rem, 5.4vw, 3.3rem)/1.03 var(--grn-12-font-display);
  letter-spacing: -0.035em;
  text-wrap: balance;
}

.grn-12__lede {
  margin: 0;
  max-inline-size: 38rem;
  color: var(--grn-12-mut);
  font-size: clamp(0.95rem, 1.9vw, 1.08rem);
  text-wrap: pretty;
}

.grn-12__cols {
  display: grid;
  gap: clamp(1rem, 2.4vw, 1.5rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  align-items: start;
}

.grn-12__card {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  gap: 1rem;
  align-content: start;
  padding: clamp(1.2rem, 3vw, 1.7rem);
  border: 1px solid var(--grn-12-line);
  border-radius: 18px;
  background: var(--grn-12-surf);
  box-shadow: inset 0 1px 0 var(--grn-12-hl), 0 12px 30px -20px rgba(20, 22, 42, 0.3);
  min-inline-size: 0;
}

.grn-12__card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: var(--grn-12-noise);
  opacity: 0.04;
  mix-blend-mode: var(--grn-12-blend);
}

.grn-12__h2 {
  margin: 0;
  padding-block-end: 0.6rem;
  border-block-end: 1px solid var(--grn-12-line);
  font: 600 1.05rem/1.2 var(--grn-12-font-display);
  letter-spacing: -0.02em;
}

.grn-12__btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.grn-12__btn {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 44px;
  padding-inline: 1.2rem;
  border: 0;
  border-radius: var(--grn-12-radius);
  background: var(--grn-12-acc);
  color: var(--grn-12-on-acc);
  font: 600 0.88rem/1 var(--grn-12-font-display);
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24), 0 2px 5px rgba(20, 22, 42, 0.22);
}

.grn-12__btn::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--grn-12-noise);
  opacity: var(--grn-12-g-idle);
  mix-blend-mode: overlay;
}

.grn-12__btn:hover::after {
  opacity: var(--grn-12-g-hover);
}

.grn-12__btn:active::after {
  opacity: var(--grn-12-g-active);
}

.grn-12__btn:active {
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.2), inset 0 2px 6px rgba(0, 0, 0, 0.3);
}

.grn-12__btn--sec {
  background: var(--grn-12-sunk);
  color: var(--grn-12-ink);
  box-shadow: inset 0 1px 0 var(--grn-12-hl), 0 1px 3px rgba(20, 22, 42, 0.14);
}

.grn-12__btn--ghost {
  background: transparent;
  color: var(--grn-12-mut);
  border: 1px solid var(--grn-12-line);
  box-shadow: none;
}

.grn-12__btn--ghost::after {
  opacity: 0;
}

.grn-12__btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}

.grn-12__seg {
  display: flex;
  flex-wrap: wrap;
  padding: 4px;
  border: 1px solid var(--grn-12-line);
  border-radius: calc(var(--grn-12-radius) + 4px);
  background: var(--grn-12-sunk);
  box-shadow: inset 0 2px 5px rgba(20, 22, 42, 0.12);
}

.grn-12__seg-in {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.grn-12__seg-lb {
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 40px;
  padding-inline: 0.8rem;
  border-radius: var(--grn-12-radius);
  color: var(--grn-12-mut);
  font: 600 0.83rem/1 var(--grn-12-font-display);
  cursor: pointer;
}

.grn-12__seg-in:checked + .grn-12__seg-lb {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--grn-12-surf);
  color: var(--grn-12-ink);
  box-shadow: inset 0 1px 0 var(--grn-12-hl), 0 1px 3px rgba(20, 22, 42, 0.18);
}

.grn-12__seg-in:checked + .grn-12__seg-lb::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--grn-12-noise);
  opacity: var(--grn-12-g-idle);
  mix-blend-mode: var(--grn-12-blend);
}

.grn-12__seg-in:focus-visible + .grn-12__seg-lb {
  outline: 2px solid var(--grn-12-acc);
  outline-offset: 2px;
}

.grn-12__field {
  display: grid;
  gap: 0.35rem;
}

.grn-12__label {
  font: 500 0.82rem/1.3 var(--grn-12-font-display);
}

.grn-12__input {
  position: relative;
  min-block-size: 46px;
  padding-inline: 0.85rem;
  border: 1px solid var(--grn-12-line);
  border-radius: var(--grn-12-radius);
  background-image: var(--grn-12-noise), linear-gradient(var(--grn-12-sunk), var(--grn-12-sunk));
  background-blend-mode: var(--grn-12-blend), normal;
  color: var(--grn-12-ink);
  font: 400 0.95rem/1 system-ui, sans-serif;
  box-shadow: inset 0 2px 5px rgba(20, 22, 42, 0.13), inset 0 -1px 0 var(--grn-12-hl);
}

.grn-12__input::placeholder {
  color: color-mix(in oklab, var(--grn-12-mut) 70%, transparent);
}

.grn-12__input:hover {
  border-color: color-mix(in oklab, var(--grn-12-acc) 45%, var(--grn-12-line));
}

.grn-12__select {
  appearance: none;
  -webkit-appearance: none;
  padding-inline-end: 2.2rem;
  background-image: var(--grn-12-noise), linear-gradient(var(--grn-12-sunk), var(--grn-12-sunk));
  cursor: pointer;
}

.grn-12__input:user-invalid {
  border-color: var(--grn-12-err);
}

.grn-12__err {
  margin: 0;
  font-size: 0.8rem;
  color: var(--grn-12-mut);
}

.grn-12__input:user-invalid ~ .grn-12__err {
  color: var(--grn-12-err);
}

.grn-12__checks {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.1rem;
}

.grn-12__checks li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-block-size: 44px;
}

.grn-12__check {
  inline-size: 20px;
  block-size: 20px;
  margin: 0;
  cursor: pointer;
}

.grn-12__checks label {
  font-size: 0.92rem;
  cursor: pointer;
}

.grn-12__range {
  inline-size: 100%;
  min-block-size: 44px;
  margin: 0;
  background: transparent;
  cursor: pointer;
}

.grn-12__hint {
  margin: 0;
  color: var(--grn-12-mut);
  font-size: 0.8rem;
  text-wrap: pretty;
}

.grn-12__btn,
.grn-12__btn::after,
.grn-12__input,
.grn-12__seg-lb {
  transition: opacity 160ms ease, box-shadow 180ms ease, border-color 180ms ease, background-color 180ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .grn-12__btn,
    .grn-12__btn::after,
    .grn-12__input,
    .grn-12__seg-lb {
    transition-duration: 1ms;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Grain and Noise Texture from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Textured Button and Input Surfaces
Source: https://codefronts.com/design-styles/css-grain-texture/textured-button-and-input-surfaces/

A complete form kit where every interactive surface is a material: buttons, inputs, a select, checkboxes, a range slider and a segmented control, all carrying the same grain token at weights tuned per state. Every control is at least 44px, every focus ring is visible, and pressing a button genuinely feels like pressing something.
## HTML
```html
<section class="grn-12" aria-labelledby="grn-12-title">
  <div class="grn-12__theme">
    <input class="grn-12__theme-input" type="checkbox" id="grn-12-theme">
    <label class="grn-12__theme-label" for="grn-12-theme">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="grn-12__sr">Toggle dark theme</span>
    </label>
  </div>

  <main class="grn-12__shell">
    <header class="grn-12__head">
      <p class="grn-12__kicker">Control surfaces · 12</p>
      <h1 class="grn-12__h1" id="grn-12-title">Every control is a material</h1>
      <p class="grn-12__lede">Buttons are raised, inputs are recessed, and both carry the same grain. Press a button and three things change at once — grain weight, highlight direction, shadow depth.</p>
    </header>

    <div class="grn-12__cols">
      <section class="grn-12__card" aria-labelledby="grn-12-h-btn">
        <h2 class="grn-12__h2" id="grn-12-h-btn">Buttons</h2>
        <div class="grn-12__btn-row">
          <button class="grn-12__btn" type="button">Save changes</button>
          <button class="grn-12__btn grn-12__btn--sec" type="button">Preview</button>
          <button class="grn-12__btn grn-12__btn--ghost" type="button">Cancel</button>
          <button class="grn-12__btn" type="button" disabled>Disabled</button>
        </div>
        <div class="grn-12__seg" role="group" aria-label="Density">
          <input class="grn-12__seg-in" type="radio" name="grn-12-density" id="grn-12-d1" checked>
          <label class="grn-12__seg-lb" for="grn-12-d1">Compact</label>
          <input class="grn-12__seg-in" type="radio" name="grn-12-density" id="grn-12-d2">
          <label class="grn-12__seg-lb" for="grn-12-d2">Regular</label>
          <input class="grn-12__seg-in" type="radio" name="grn-12-density" id="grn-12-d3">
          <label class="grn-12__seg-lb" for="grn-12-d3">Roomy</label>
        </div>
        <p class="grn-12__hint">Idle 0.05 · hover 0.06 · active 0.08 with the highlight inverted.</p>
      </section>

      <section class="grn-12__card" aria-labelledby="grn-12-h-fields">
        <h2 class="grn-12__h2" id="grn-12-h-fields">Fields</h2>
        <div class="grn-12__field">
          <label class="grn-12__label" for="grn-12-name">Full name</label>
          <input class="grn-12__input" type="text" id="grn-12-name" name="grn-12-name" placeholder="Ada Lovelace" autocomplete="name">
        </div>
        <div class="grn-12__field">
          <label class="grn-12__label" for="grn-12-mail">Work email</label>
          <input class="grn-12__input" type="email" id="grn-12-mail" name="grn-12-mail" placeholder="ada@example.com" autocomplete="email" required aria-describedby="grn-12-mail-hint">
          <p class="grn-12__err" id="grn-12-mail-hint">We only email about deploys. Real address required.</p>
        </div>
        <div class="grn-12__field">
          <label class="grn-12__label" for="grn-12-region">Region</label>
          <select class="grn-12__input grn-12__select" id="grn-12-region" name="grn-12-region">
            <option>eu-west-2 · London</option>
            <option>us-east-1 · Virginia</option>
            <option>ap-southeast-2 · Sydney</option>
          </select>
        </div>
      </section>

      <section class="grn-12__card" aria-labelledby="grn-12-h-misc">
        <h2 class="grn-12__h2" id="grn-12-h-misc">Toggles and ranges</h2>
        <ul class="grn-12__checks">
          <li><input class="grn-12__check" type="checkbox" id="grn-12-c1" checked><label for="grn-12-c1">Textured surfaces</label></li>
          <li><input class="grn-12__check" type="checkbox" id="grn-12-c2"><label for="grn-12-c2">Reduce transparency</label></li>
          <li><input class="grn-12__check" type="radio" name="grn-12-r" id="grn-12-r1" checked><label for="grn-12-r1">Match system theme</label></li>
          <li><input class="grn-12__check" type="radio" name="grn-12-r" id="grn-12-r2"><label for="grn-12-r2">Always dark</label></li>
        </ul>
        <div class="grn-12__field">
          <label class="grn-12__label" for="grn-12-grain">Grain weight</label>
          <input class="grn-12__range" type="range" id="grn-12-grain" name="grn-12-grain" min="0" max="20" value="6" step="1">
          <p class="grn-12__hint">Native thumb, textured track, <code>accent-color</code> for the fill.</p>
        </div>
      </section>
    </div>
  </main>
</section>
```
## CSS
```css
.grn-12 {
  --grn-12-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='grn12n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23grn12n)'/%3E%3C/svg%3E");
  --grn-12-bg: #eef0f7;
  --grn-12-surf: #ffffff;
  --grn-12-sunk: #e7eaf4;
  --grn-12-line: #d3d8e8;
  --grn-12-ink: #14162a;
  --grn-12-mut: #5c6280;
  --grn-12-acc: #4f46e5;
  --grn-12-acc: oklch(52% 0.22 275);
  --grn-12-on-acc: #f2f1fe;
  --grn-12-err: #b42318;
  --grn-12-err: oklch(50% 0.18 27);
  --grn-12-g-idle: 0.05;
  --grn-12-g-hover: 0.06;
  --grn-12-g-active: 0.08;
  --grn-12-g-sunk: 0.035;
  --grn-12-blend: multiply;
  --grn-12-hl: rgba(255, 255, 255, 0.85);
  --grn-12-radius: 12px;
  --grn-12-font-display: "Space Grotesk", "Inter Tight", ui-sans-serif, system-ui, sans-serif;
  --grn-12-font-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", Menlo, monospace;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--grn-12-bg);
  color: var(--grn-12-ink);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  line-height: 1.55;
  accent-color: var(--grn-12-acc);
}

.grn-12 *,
.grn-12 *::before,
.grn-12 *::after {
  box-sizing: border-box;
}

.grn-12 :focus-visible {
  outline: 2px solid var(--grn-12-acc);
  outline-offset: 2px;
}

.grn-12 code {
  font-family: var(--grn-12-font-mono);
  font-size: 0.9em;
}

.grn-12__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.grn-12:has(.grn-12__theme-input:checked) {
  --grn-12-bg: #0c0d16;
  --grn-12-surf: #15172a;
  --grn-12-sunk: #10121f;
  --grn-12-line: #262a45;
  --grn-12-ink: #ecebfb;
  --grn-12-mut: #9a9ac4;
  --grn-12-acc: #a5b4fc;
  --grn-12-acc: oklch(80% 0.11 278);
  --grn-12-on-acc: #10122a;
  --grn-12-err: #fca5a5;
  --grn-12-err: oklch(78% 0.13 22);
  --grn-12-blend: soft-light;
  --grn-12-hl: rgba(255, 255, 255, 0.12);
  --grn-12-g-idle: 0.07;
  --grn-12-g-hover: 0.085;
  --grn-12-g-active: 0.11;
  --grn-12-g-sunk: 0.05;
}

@media (prefers-color-scheme: dark) {
  .grn-12 {
    --grn-12-bg: #0c0d16;
    --grn-12-surf: #15172a;
    --grn-12-sunk: #10121f;
    --grn-12-line: #262a45;
    --grn-12-ink: #ecebfb;
    --grn-12-mut: #9a9ac4;
    --grn-12-acc: #a5b4fc;
    --grn-12-acc: oklch(80% 0.11 278);
    --grn-12-on-acc: #10122a;
    --grn-12-err: #fca5a5;
    --grn-12-err: oklch(78% 0.13 22);
    --grn-12-blend: soft-light;
    --grn-12-hl: rgba(255, 255, 255, 0.12);
    --grn-12-g-idle: 0.07;
    --grn-12-g-hover: 0.085;
    --grn-12-g-active: 0.11;
    --grn-12-g-sunk: 0.05;
  }

  .grn-12:has(.grn-12__theme-input:checked) {
    --grn-12-bg: #eef0f7;
    --grn-12-surf: #ffffff;
    --grn-12-sunk: #e7eaf4;
    --grn-12-line: #d3d8e8;
    --grn-12-ink: #14162a;
    --grn-12-mut: #5c6280;
    --grn-12-acc: #4f46e5;
    --grn-12-acc: oklch(52% 0.22 275);
    --grn-12-on-acc: #f2f1fe;
    --grn-12-err: #b42318;
    --grn-12-err: oklch(50% 0.18 27);
    --grn-12-blend: multiply;
    --grn-12-hl: rgba(255, 255, 255, 0.85);
    --grn-12-g-idle: 0.05;
    --grn-12-g-hover: 0.06;
    --grn-12-g-active: 0.08;
    --grn-12-g-sunk: 0.035;
  }
}

@media (prefers-color-scheme: light) {
  .grn-12:has(.grn-12__theme-input:checked) {
    --grn-12-bg: #0c0d16;
    --grn-12-surf: #15172a;
    --grn-12-sunk: #10121f;
    --grn-12-line: #262a45;
    --grn-12-ink: #ecebfb;
    --grn-12-mut: #9a9ac4;
    --grn-12-acc: #a5b4fc;
    --grn-12-acc: oklch(80% 0.11 278);
    --grn-12-on-acc: #10122a;
    --grn-12-err: #fca5a5;
    --grn-12-err: oklch(78% 0.13 22);
    --grn-12-blend: soft-light;
    --grn-12-hl: rgba(255, 255, 255, 0.12);
    --grn-12-g-idle: 0.07;
    --grn-12-g-hover: 0.085;
    --grn-12-g-active: 0.11;
    --grn-12-g-sunk: 0.05;
  }
}

.grn-12__theme {
  position: absolute;
  inset-block-start: clamp(1rem, 3vw, 1.75rem);
  inset-inline-end: clamp(0.85rem, 3vw, 1.75rem);
  z-index: 40;
}

.grn-12__theme-input {
  position: absolute;
  inset: 0;
  opacity: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  cursor: pointer;
}

.grn-12__theme-label {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid var(--grn-12-line);
  border-radius: 999px;
  background: var(--grn-12-surf);
  color: var(--grn-12-ink);
  cursor: pointer;
  box-shadow: inset 0 1px 0 var(--grn-12-hl);
}

.grn-12__theme-label svg {
  inline-size: 20px;
  block-size: 20px;
}

.grn-12__shell {
  inline-size: min(76rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
  margin-inline: auto;
  padding-block: clamp(3rem, 8vw, 5.5rem) clamp(2.5rem, 6vw, 4rem);
  display: grid;
  gap: clamp(2rem, 5vw, 3rem);
}

.grn-12__head {
  max-inline-size: 44rem;
  display: grid;
  gap: 0.6rem;
}

.grn-12__kicker {
  margin: 0;
  font: 500 0.72rem/1 var(--grn-12-font-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--grn-12-acc);
}

.grn-12__h1 {
  margin: 0;
  font: 600 clamp(2rem, 5.4vw, 3.3rem)/1.03 var(--grn-12-font-display);
  letter-spacing: -0.035em;
  text-wrap: balance;
}

.grn-12__lede {
  margin: 0;
  max-inline-size: 38rem;
  color: var(--grn-12-mut);
  font-size: clamp(0.95rem, 1.9vw, 1.08rem);
  text-wrap: pretty;
}

.grn-12__cols {
  display: grid;
  gap: clamp(1rem, 2.4vw, 1.5rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  align-items: start;
}

.grn-12__card {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  gap: 1rem;
  align-content: start;
  padding: clamp(1.2rem, 3vw, 1.7rem);
  border: 1px solid var(--grn-12-line);
  border-radius: 18px;
  background: var(--grn-12-surf);
  box-shadow: inset 0 1px 0 var(--grn-12-hl), 0 12px 30px -20px rgba(20, 22, 42, 0.3);
  min-inline-size: 0;
}

.grn-12__card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: var(--grn-12-noise);
  opacity: 0.04;
  mix-blend-mode: var(--grn-12-blend);
}

.grn-12__h2 {
  margin: 0;
  padding-block-end: 0.6rem;
  border-block-end: 1px solid var(--grn-12-line);
  font: 600 1.05rem/1.2 var(--grn-12-font-display);
  letter-spacing: -0.02em;
}

.grn-12__btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.grn-12__btn {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 44px;
  padding-inline: 1.2rem;
  border: 0;
  border-radius: var(--grn-12-radius);
  background: var(--grn-12-acc);
  color: var(--grn-12-on-acc);
  font: 600 0.88rem/1 var(--grn-12-font-display);
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24), 0 2px 5px rgba(20, 22, 42, 0.22);
}

.grn-12__btn::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--grn-12-noise);
  opacity: var(--grn-12-g-idle);
  mix-blend-mode: overlay;
}

.grn-12__btn:hover::after {
  opacity: var(--grn-12-g-hover);
}

.grn-12__btn:active::after {
  opacity: var(--grn-12-g-active);
}

.grn-12__btn:active {
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.2), inset 0 2px 6px rgba(0, 0, 0, 0.3);
}

.grn-12__btn--sec {
  background: var(--grn-12-sunk);
  color: var(--grn-12-ink);
  box-shadow: inset 0 1px 0 var(--grn-12-hl), 0 1px 3px rgba(20, 22, 42, 0.14);
}

.grn-12__btn--ghost {
  background: transparent;
  color: var(--grn-12-mut);
  border: 1px solid var(--grn-12-line);
  box-shadow: none;
}

.grn-12__btn--ghost::after {
  opacity: 0;
}

.grn-12__btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}

.grn-12__seg {
  display: flex;
  flex-wrap: wrap;
  padding: 4px;
  border: 1px solid var(--grn-12-line);
  border-radius: calc(var(--grn-12-radius) + 4px);
  background: var(--grn-12-sunk);
  box-shadow: inset 0 2px 5px rgba(20, 22, 42, 0.12);
}

.grn-12__seg-in {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.grn-12__seg-lb {
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 40px;
  padding-inline: 0.8rem;
  border-radius: var(--grn-12-radius);
  color: var(--grn-12-mut);
  font: 600 0.83rem/1 var(--grn-12-font-display);
  cursor: pointer;
}

.grn-12__seg-in:checked + .grn-12__seg-lb {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--grn-12-surf);
  color: var(--grn-12-ink);
  box-shadow: inset 0 1px 0 var(--grn-12-hl), 0 1px 3px rgba(20, 22, 42, 0.18);
}

.grn-12__seg-in:checked + .grn-12__seg-lb::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--grn-12-noise);
  opacity: var(--grn-12-g-idle);
  mix-blend-mode: var(--grn-12-blend);
}

.grn-12__seg-in:focus-visible + .grn-12__seg-lb {
  outline: 2px solid var(--grn-12-acc);
  outline-offset: 2px;
}

.grn-12__field {
  display: grid;
  gap: 0.35rem;
}

.grn-12__label {
  font: 500 0.82rem/1.3 var(--grn-12-font-display);
}

.grn-12__input {
  position: relative;
  min-block-size: 46px;
  padding-inline: 0.85rem;
  border: 1px solid var(--grn-12-line);
  border-radius: var(--grn-12-radius);
  background-image: var(--grn-12-noise), linear-gradient(var(--grn-12-sunk), var(--grn-12-sunk));
  background-blend-mode: var(--grn-12-blend), normal;
  color: var(--grn-12-ink);
  font: 400 0.95rem/1 system-ui, sans-serif;
  box-shadow: inset 0 2px 5px rgba(20, 22, 42, 0.13), inset 0 -1px 0 var(--grn-12-hl);
}

.grn-12__input::placeholder {
  color: color-mix(in oklab, var(--grn-12-mut) 70%, transparent);
}

.grn-12__input:hover {
  border-color: color-mix(in oklab, var(--grn-12-acc) 45%, var(--grn-12-line));
}

.grn-12__select {
  appearance: none;
  -webkit-appearance: none;
  padding-inline-end: 2.2rem;
  background-image: var(--grn-12-noise), linear-gradient(var(--grn-12-sunk), var(--grn-12-sunk));
  cursor: pointer;
}

.grn-12__input:user-invalid {
  border-color: var(--grn-12-err);
}

.grn-12__err {
  margin: 0;
  font-size: 0.8rem;
  color: var(--grn-12-mut);
}

.grn-12__input:user-invalid ~ .grn-12__err {
  color: var(--grn-12-err);
}

.grn-12__checks {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.1rem;
}

.grn-12__checks li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-block-size: 44px;
}

.grn-12__check {
  inline-size: 20px;
  block-size: 20px;
  margin: 0;
  cursor: pointer;
}

.grn-12__checks label {
  font-size: 0.92rem;
  cursor: pointer;
}

.grn-12__range {
  inline-size: 100%;
  min-block-size: 44px;
  margin: 0;
  background: transparent;
  cursor: pointer;
}

.grn-12__hint {
  margin: 0;
  color: var(--grn-12-mut);
  font-size: 0.8rem;
  text-wrap: pretty;
}

.grn-12__btn,
.grn-12__btn::after,
.grn-12__input,
.grn-12__seg-lb {
  transition: opacity 160ms ease, box-shadow 180ms ease, border-color 180ms ease, background-color 180ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .grn-12__btn,
    .grn-12__btn::after,
    .grn-12__input,
    .grn-12__seg-lb {
    transition-duration: 1ms;
  }
}
```

How this works

Texture makes state legible. A flat button communicates hover with a colour shift and press with a translate, both of which are weak signals. A textured button can change three things at once — grain weight, highlight direction and shadow depth — which the eye reads as a single physical event. The grain going from 0.05 to 0.07 is imperceptible on its own; combined with the highlight moving from the top edge to the bottom edge, it reads unmistakably as depression.

Inputs need the opposite lighting to buttons. A button is a raised surface: highlight on top, shadow below. An input is a recess: shadow on top inside, highlight along the bottom inside. Getting this backwards is the most common reason a form looks subtly wrong without anybody being able to say why. Grain sits on both, at slightly lower weight inside the recess, because a recessed surface catches less light.

Keep native controls native. accent-color tints checkboxes, radios and range thumbs with one declaration and preserves every piece of built-in accessibility and platform behaviour. Where you need more — a textured range track, for instance — style the track and leave the thumb to the platform. Rebuilding a checkbox from a div is how forms end up unusable with a screen reader, and the visual gain is close to zero.

Validation timing matters more than validation styling. :invalid matches on first paint, so an empty required field is red before the user has done anything. :user-invalid matches only after interaction, which is the behaviour every designer actually intends. Pair it with a real aria-describedby hint rather than colour alone, since colour is not a sufficient error signal.

Make it yours

  • Change --grn-12-g-idle, -hover and -active to tune how strongly the press reads; the three-step ramp is the mechanism.
  • Swap accent-color for your brand colour and the checkboxes, radios and slider thumb follow automatically.
  • Increase the input recess shadow for a more skeuomorphic field, or drop it entirely for a flat-with-texture look.
  • Add the same grain token to your existing button component and adjust nothing else — the weights here are tuned to be theme-neutral.
  • Change --grn-12-radius to reshape every control at once; the highlights and shadows are radius-agnostic.
  • Recolour with the --grn-12-acc pair, hex first then oklch.

Gotchas — read before shipping

  • Grain on a small control at the same weight as a large surface looks about twice as strong. Buttons need a lower number than cards.
  • Never remove the focus ring to make a textured button look cleaner. Use :focus-visible so pointer users never see it and keyboard users always do.
  • :invalid styles a required empty field red before interaction. Use :user-invalid.
  • A pressed state built only from transform: translateY shifts the text and can trigger sub-pixel re-rasterisation. Move the highlight instead.
  • Do not put grain on the ::placeholder layer or inside the text area of an input — texture behind small type reduces legibility, which is a real accessibility cost.
  • appearance: none on a checkbox removes platform accessibility affordances in some assistive tech. accent-color achieves the styling without it.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

accent-color is supported in Chrome 93+, Safari 15.4+, Firefox 92+. :user-invalid is Chrome 119+, Safari 16.5+, Firefox 88+ and simply does not match in older engines, so validation styling degrades to no styling rather than to wrong styling. The version floor is oklch() and :has().

Techniques used in this demo

Search CodeFronts

Loading…