20 CSS Popovers14 / 20

Pure CSSMIT licensed

Date Picker Popover Panel

A calendar panel tethered to a date field, with the month laid out as a plain CSS grid and no date logic at all. The demo is the panel and its tethering: how a popover attaches to an input, how the grid stays legible at 320px, and why the field needs readonly or a real picker behind it before this ships.

Published

Live Demo
Try it

The code

<div class="pop-14">
  <div class="pop-14__stage">
    <div class="pop-14__form">
      <p class="pop-14__kicker">Booking · Studio 2</p>
      <label class="pop-14__label" for="pop-14-date">Session date</label>
      <div class="pop-14__field">
        <input class="pop-14__input" id="pop-14-date" type="text" value="14 September 2026" readonly aria-describedby="pop-14-help">
        <button class="pop-14__icon" type="button" popovertarget="pop-14-cal" aria-label="Choose a date">
          <svg viewBox="0 0 20 20" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="3" y="4.6" width="14" height="12" rx="2"/><path d="M3 8.4h14M7 3v2.6M13 3v2.6"/></svg>
        </button>
      </div>
      <p class="pop-14__help" id="pop-14-help">The field is the anchor. The icon button is only the invoker.</p>
    </div>
  </div>

  <div class="pop-14__cal" popover="auto" id="pop-14-cal" role="group" aria-label="September 2026">
    <div class="pop-14__calhead">
      <span class="pop-14__month">September 2026</span>
      <span class="pop-14__static">static month</span>
    </div>
    <div class="pop-14__dows" aria-hidden="true">
      <span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span><span>S</span>
    </div>
    <div class="pop-14__grid" role="grid" aria-label="Days in September 2026">
      <span class="pop-14__day pop-14__day--mute">31</span>
      <span class="pop-14__day">1</span><span class="pop-14__day">2</span><span class="pop-14__day">3</span><span class="pop-14__day">4</span><span class="pop-14__day">5</span><span class="pop-14__day">6</span>
      <span class="pop-14__day">7</span><span class="pop-14__day">8</span><span class="pop-14__day">9</span><span class="pop-14__day">10</span><span class="pop-14__day">11</span><span class="pop-14__day">12</span><span class="pop-14__day">13</span>
      <span class="pop-14__day pop-14__day--sel" aria-selected="true">14</span><span class="pop-14__day">15</span><span class="pop-14__day">16</span><span class="pop-14__day pop-14__day--now" aria-current="date">17</span><span class="pop-14__day">18</span><span class="pop-14__day">19</span><span class="pop-14__day">20</span>
      <span class="pop-14__day">21</span><span class="pop-14__day">22</span><span class="pop-14__day">23</span><span class="pop-14__day">24</span><span class="pop-14__day">25</span><span class="pop-14__day">26</span><span class="pop-14__day">27</span>
      <span class="pop-14__day">28</span><span class="pop-14__day">29</span><span class="pop-14__day">30</span>
      <span class="pop-14__day pop-14__day--mute">1</span><span class="pop-14__day pop-14__day--mute">2</span><span class="pop-14__day pop-14__day--mute">3</span><span class="pop-14__day pop-14__day--mute">4</span>
    </div>
  </div>
</div>
.pop-14 {
  width: 100%;
  min-height: 100vh;
  display: block;
  box-sizing: border-box;
  --bg: #f0ebe3;
  --surface: #fffdf9;
  --ink: #26221c;
  --muted: #7b7062;
  --rule: #ddd4c6;
  --sel: #3f6a4e;
  --font-ui: ui-sans-serif, system-ui, "Segoe UI", sans-serif;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  padding: 40px 18px;
}

.pop-14__stage {
  display: grid;
  place-items: center;
  max-inline-size: 100%;
  min-height: calc(100vh - 80px);
}

.pop-14__form {
  inline-size: 100%;
  max-inline-size: 24rem;
  min-inline-size: 0;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 20px 18px 18px;
}

.pop-14__kicker {
  margin: 0 0 16px;
  font: 700 10px/1 var(--font-ui);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sel);
}

.pop-14__label {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.pop-14__field {
  display: flex;
  align-items: center;
  gap: 4px;
  padding-inline-end: 4px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 7px;
  anchor-name: --pop-14-anchor;
}

.pop-14__field:focus-within {
  border-color: var(--sel);
}

.pop-14__input {
  flex: 1 1 auto;
  min-inline-size: 0;
  min-height: 44px;
  padding: 0 12px;
  font: 500 13.5px/1 var(--font-ui);
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 7px 0 0 7px;
}

.pop-14__input:focus-visible {
  outline: 3px solid var(--sel);
  outline-offset: -1px;
}

.pop-14__icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
}

.pop-14__icon:hover {
  color: var(--sel);
  background: var(--bg);
}

.pop-14__icon:focus-visible {
  outline: 3px solid var(--sel);
  outline-offset: -2px;
}

.pop-14__help {
  margin: 9px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
}

.pop-14__cal {
  position: fixed;
  margin: 0;
  inline-size: min(20rem, calc(100vw - 28px));
  max-inline-size: calc(100vw - 28px);
  padding: 14px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  box-shadow: 0 22px 44px -26px rgba(38, 34, 28, 0.5);
}

@supports (anchor-name: --a) {
  .pop-14__cal {
    position-anchor: --pop-14-anchor;
    position-area: block-end span-inline-end;
    inset: auto;
    margin-block-start: 7px;
    position-try-fallbacks: flip-block;
  }
}

.pop-14__calhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 12px;
}

.pop-14__month {
  font-size: 13px;
  font-weight: 650;
}

.pop-14__static {
  font: 600 9.5px/1 var(--font-ui);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.pop-14__dows,
.pop-14__grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 2px;
}

.pop-14__dows {
  margin-bottom: 4px;
}

.pop-14__dows span {
  text-align: center;
  font: 700 10px/1 var(--font-ui);
  color: var(--muted);
}

.pop-14__day {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  min-height: 34px;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  border-radius: 6px;
}

.pop-14__day--mute {
  color: color-mix(in oklab, var(--muted) 55%, transparent);
}

.pop-14__day--now {
  box-shadow: inset 0 0 0 1px var(--muted);
}

.pop-14__day--sel {
  font-weight: 700;
  color: var(--surface);
  background: var(--sel);
  box-shadow: inset 0 0 0 2px var(--sel);
}

@media (min-width: 420px) {
  .pop-14__day {
    min-height: 38px;
  }
}

@media (prefers-color-scheme: dark) {
  .pop-14 {
    --bg: #17140f;
    --surface: #211d17;
    --ink: #f2ece1;
    --muted: #a39682;
    --rule: #362f26;
    --sel: #7fb894;
  }

  .pop-14__day--sel {
    color: #17140f;
  }
}

[data-theme="dark"] .pop-14 {
  --bg: #17140f;
  --surface: #211d17;
  --ink: #f2ece1;
  --muted: #a39682;
  --rule: #362f26;
  --sel: #7fb894;
}

[data-theme="dark"] .pop-14 .pop-14__day--sel {
  color: #17140f;
}
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 Popover 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: Date Picker Popover Panel
Source: https://codefronts.com/snippets/css-popovers/date-picker-popover-panel/

A calendar panel tethered to a date field, with the month laid out as a plain CSS grid and no date logic at all. The demo is the panel and its tethering: how a popover attaches to an input, how the grid stays legible at 320px, and why the field needs readonly or a real picker behind it before this ships.
## HTML
```html
<div class="pop-14">
  <div class="pop-14__stage">
    <div class="pop-14__form">
      <p class="pop-14__kicker">Booking · Studio 2</p>
      <label class="pop-14__label" for="pop-14-date">Session date</label>
      <div class="pop-14__field">
        <input class="pop-14__input" id="pop-14-date" type="text" value="14 September 2026" readonly aria-describedby="pop-14-help">
        <button class="pop-14__icon" type="button" popovertarget="pop-14-cal" aria-label="Choose a date">
          <svg viewBox="0 0 20 20" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="3" y="4.6" width="14" height="12" rx="2"/><path d="M3 8.4h14M7 3v2.6M13 3v2.6"/></svg>
        </button>
      </div>
      <p class="pop-14__help" id="pop-14-help">The field is the anchor. The icon button is only the invoker.</p>
    </div>
  </div>

  <div class="pop-14__cal" popover="auto" id="pop-14-cal" role="group" aria-label="September 2026">
    <div class="pop-14__calhead">
      <span class="pop-14__month">September 2026</span>
      <span class="pop-14__static">static month</span>
    </div>
    <div class="pop-14__dows" aria-hidden="true">
      <span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span><span>S</span>
    </div>
    <div class="pop-14__grid" role="grid" aria-label="Days in September 2026">
      <span class="pop-14__day pop-14__day--mute">31</span>
      <span class="pop-14__day">1</span><span class="pop-14__day">2</span><span class="pop-14__day">3</span><span class="pop-14__day">4</span><span class="pop-14__day">5</span><span class="pop-14__day">6</span>
      <span class="pop-14__day">7</span><span class="pop-14__day">8</span><span class="pop-14__day">9</span><span class="pop-14__day">10</span><span class="pop-14__day">11</span><span class="pop-14__day">12</span><span class="pop-14__day">13</span>
      <span class="pop-14__day pop-14__day--sel" aria-selected="true">14</span><span class="pop-14__day">15</span><span class="pop-14__day">16</span><span class="pop-14__day pop-14__day--now" aria-current="date">17</span><span class="pop-14__day">18</span><span class="pop-14__day">19</span><span class="pop-14__day">20</span>
      <span class="pop-14__day">21</span><span class="pop-14__day">22</span><span class="pop-14__day">23</span><span class="pop-14__day">24</span><span class="pop-14__day">25</span><span class="pop-14__day">26</span><span class="pop-14__day">27</span>
      <span class="pop-14__day">28</span><span class="pop-14__day">29</span><span class="pop-14__day">30</span>
      <span class="pop-14__day pop-14__day--mute">1</span><span class="pop-14__day pop-14__day--mute">2</span><span class="pop-14__day pop-14__day--mute">3</span><span class="pop-14__day pop-14__day--mute">4</span>
    </div>
  </div>
</div>
```
## CSS
```css
.pop-14 {
  width: 100%;
  min-height: 100vh;
  display: block;
  box-sizing: border-box;
  --bg: #f0ebe3;
  --surface: #fffdf9;
  --ink: #26221c;
  --muted: #7b7062;
  --rule: #ddd4c6;
  --sel: #3f6a4e;
  --font-ui: ui-sans-serif, system-ui, "Segoe UI", sans-serif;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  padding: 40px 18px;
}

.pop-14__stage {
  display: grid;
  place-items: center;
  max-inline-size: 100%;
  min-height: calc(100vh - 80px);
}

.pop-14__form {
  inline-size: 100%;
  max-inline-size: 24rem;
  min-inline-size: 0;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 20px 18px 18px;
}

.pop-14__kicker {
  margin: 0 0 16px;
  font: 700 10px/1 var(--font-ui);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sel);
}

.pop-14__label {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.pop-14__field {
  display: flex;
  align-items: center;
  gap: 4px;
  padding-inline-end: 4px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 7px;
  anchor-name: --pop-14-anchor;
}

.pop-14__field:focus-within {
  border-color: var(--sel);
}

.pop-14__input {
  flex: 1 1 auto;
  min-inline-size: 0;
  min-height: 44px;
  padding: 0 12px;
  font: 500 13.5px/1 var(--font-ui);
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 7px 0 0 7px;
}

.pop-14__input:focus-visible {
  outline: 3px solid var(--sel);
  outline-offset: -1px;
}

.pop-14__icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
}

.pop-14__icon:hover {
  color: var(--sel);
  background: var(--bg);
}

.pop-14__icon:focus-visible {
  outline: 3px solid var(--sel);
  outline-offset: -2px;
}

.pop-14__help {
  margin: 9px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
}

.pop-14__cal {
  position: fixed;
  margin: 0;
  inline-size: min(20rem, calc(100vw - 28px));
  max-inline-size: calc(100vw - 28px);
  padding: 14px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  box-shadow: 0 22px 44px -26px rgba(38, 34, 28, 0.5);
}

@supports (anchor-name: --a) {
  .pop-14__cal {
    position-anchor: --pop-14-anchor;
    position-area: block-end span-inline-end;
    inset: auto;
    margin-block-start: 7px;
    position-try-fallbacks: flip-block;
  }
}

.pop-14__calhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 12px;
}

.pop-14__month {
  font-size: 13px;
  font-weight: 650;
}

.pop-14__static {
  font: 600 9.5px/1 var(--font-ui);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.pop-14__dows,
.pop-14__grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 2px;
}

.pop-14__dows {
  margin-bottom: 4px;
}

.pop-14__dows span {
  text-align: center;
  font: 700 10px/1 var(--font-ui);
  color: var(--muted);
}

.pop-14__day {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  min-height: 34px;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  border-radius: 6px;
}

.pop-14__day--mute {
  color: color-mix(in oklab, var(--muted) 55%, transparent);
}

.pop-14__day--now {
  box-shadow: inset 0 0 0 1px var(--muted);
}

.pop-14__day--sel {
  font-weight: 700;
  color: var(--surface);
  background: var(--sel);
  box-shadow: inset 0 0 0 2px var(--sel);
}

@media (min-width: 420px) {
  .pop-14__day {
    min-height: 38px;
  }
}

@media (prefers-color-scheme: dark) {
  .pop-14 {
    --bg: #17140f;
    --surface: #211d17;
    --ink: #f2ece1;
    --muted: #a39682;
    --rule: #362f26;
    --sel: #7fb894;
  }

  .pop-14__day--sel {
    color: #17140f;
  }
}

[data-theme="dark"] .pop-14 {
  --bg: #17140f;
  --surface: #211d17;
  --ink: #f2ece1;
  --muted: #a39682;
  --rule: #362f26;
  --sel: #7fb894;
}

[data-theme="dark"] .pop-14 .pop-14__day--sel {
  color: #17140f;
}
```

How this works

An input can be an anchor. Put anchor-name on the field itself, not on the icon button beside it, and the panel aligns to the whole control — which is what users read as "attached". The icon button is only the invoker: it carries popovertarget, and because it sits inside the field's wrapper the two read as one component.

The month is a grid, not a table of pixels. Seven columns, one cell per day, font-variant-numeric: tabular-nums so the columns do not wobble between 1 and 11, and aspect-ratio on the cells so the grid stays square as the panel narrows. Day targets clear 44px on a phone and the whole panel still fits inside a 320px viewport.

Semantics come from you. The panel is a role="group" with a label naming the month; today carries aria-current="date"; the chosen day carries aria-selected and a visible weight change, not just a colour. Popover contributes nothing here — it has no implicit role — and a calendar with colour-only selection fails on both contrast and colourblind grounds.

Ship it with a real input behind it. A text field that opens a custom calendar must either be readonly, so typing cannot desynchronise from the panel, or be backed by a proper parser and a native <input type="date"> fallback. This demo is static on purpose: a working date library is a different problem from a correctly tethered panel.

Make it yours

  • Change position-area to block-start span-inline-end to open the calendar upward.
  • Set inline-size: anchor-size(width) to make the calendar exactly as wide as the field.
  • Swap the six-row grid for five if your month range never wraps.
  • Retint the selected day by editing --sel.
  • Add a footer row with Today and Clear as real buttons.
  • Increase the cell aspect-ratio to give the grid a wider, more compact look.

Gotchas — read before shipping

  • Anchoring to the icon button instead of the field makes the panel hang off the icon and look detached from the control.
  • A day grid without tabular numerals shifts column widths between single and double digit dates.
  • Marking selection with background colour only fails contrast and colourblind checks — keep the weight or the ring.
  • An editable text field paired with a custom picker desynchronises the moment the user types; use readonly or parse the input.

Browser support

ChromeSafariFirefoxEdge
125+26+131+125+

Tethering the calendar to the field needs anchor positioning (Chrome 125, Safari 26, Firefox 131); the popover attribute that opens it is Chrome 114, Safari 17, Firefox 125. Unanchored, the calendar opens centred in the viewport, which on a phone is close to what a native picker does anyway — so the fallback is genuinely usable rather than merely present.

Techniques used in this demo

Search CodeFronts

Loading…