// https://github.com/tailwindlabs/tailwindcss-forms/commit/c9d9da3e010b194a1f0e9c36fbd98c83e4762840
import plugin from "tailwindcss/plugin";
import defaultTheme from "tailwindcss/defaultTheme";
import colors from "tailwindcss/colors";
import type { CSSRuleObject } from "tailwindcss/types/config";
const shorterNames = {
aqua: /#00ffff(ff)?(?!\w)|#0ff(f)?(?!\w)/gi,
azure: /#f0ffff(ff)?(?!\w)/gi,
beige: /#f5f5dc(ff)?(?!\w)/gi,
bisque: /#ffe4c4(ff)?(?!\w)/gi,
black: /#000000(ff)?(?!\w)|#000(f)?(?!\w)/gi,
blue: /#0000ff(ff)?(?!\w)|#00f(f)?(?!\w)/gi,
brown: /#a52a2a(ff)?(?!\w)/gi,
coral: /#ff7f50(ff)?(?!\w)/gi,
cornsilk: /#fff8dc(ff)?(?!\w)/gi,
crimson: /#dc143c(ff)?(?!\w)/gi,
cyan: /#00ffff(ff)?(?!\w)|#0ff(f)?(?!\w)/gi,
darkblue: /#00008b(ff)?(?!\w)/gi,
darkcyan: /#008b8b(ff)?(?!\w)/gi,
darkgrey: /#a9a9a9(ff)?(?!\w)/gi,
darkred: /#8b0000(ff)?(?!\w)/gi,
deeppink: /#ff1493(ff)?(?!\w)/gi,
dimgrey: /#696969(ff)?(?!\w)/gi,
gold: /#ffd700(ff)?(?!\w)/gi,
green: /#008000(ff)?(?!\w)/gi,
grey: /#808080(ff)?(?!\w)/gi,
honeydew: /#f0fff0(ff)?(?!\w)/gi,
hotpink: /#ff69b4(ff)?(?!\w)/gi,
indigo: /#4b0082(ff)?(?!\w)/gi,
ivory: /#fffff0(ff)?(?!\w)/gi,
khaki: /#f0e68c(ff)?(?!\w)/gi,
lavender: /#e6e6fa(ff)?(?!\w)/gi,
lime: /#00ff00(ff)?(?!\w)|#0f0(f)?(?!\w)/gi,
linen: /#faf0e6(ff)?(?!\w)/gi,
maroon: /#800000(ff)?(?!\w)/gi,
moccasin: /#ffe4b5(ff)?(?!\w)/gi,
navy: /#000080(ff)?(?!\w)/gi,
oldlace: /#fdf5e6(ff)?(?!\w)/gi,
olive: /#808000(ff)?(?!\w)/gi,
orange: /#ffa500(ff)?(?!\w)/gi,
orchid: /#da70d6(ff)?(?!\w)/gi,
peru: /#cd853f(ff)?(?!\w)/gi,
pink: /#ffc0cb(ff)?(?!\w)/gi,
plum: /#dda0dd(ff)?(?!\w)/gi,
purple: /#800080(ff)?(?!\w)/gi,
red: /#ff0000(ff)?(?!\w)|#f00(f)?(?!\w)/gi,
salmon: /#fa8072(ff)?(?!\w)/gi,
seagreen: /#2e8b57(ff)?(?!\w)/gi,
seashell: /#fff5ee(ff)?(?!\w)/gi,
sienna: /#a0522d(ff)?(?!\w)/gi,
silver: /#c0c0c0(ff)?(?!\w)/gi,
skyblue: /#87ceeb(ff)?(?!\w)/gi,
snow: /#fffafa(ff)?(?!\w)/gi,
tan: /#d2b48c(ff)?(?!\w)/gi,
teal: /#008080(ff)?(?!\w)/gi,
thistle: /#d8bfd8(ff)?(?!\w)/gi,
tomato: /#ff6347(ff)?(?!\w)/gi,
violet: /#ee82ee(ff)?(?!\w)/gi,
wheat: /#f5deb3(ff)?(?!\w)/gi,
white: /#ffffff(ff)?(?!\w)|#fff(f)?(?!\w)/gi,
};
const REGEX = {
whitespace: /\s+/g,
urlHexPairs: /%[\dA-F]{2}/g,
quotes: /"/g,
};
function collapseWhitespace(str: string) {
return str.trim().replace(REGEX.whitespace, " ");
}
function dataURIPayload(string: string) {
return encodeURIComponent(string).replace(REGEX.urlHexPairs, specialHexEncode);
}
// `#` gets converted to `%23`, so quite a few CSS named colors are shorter than
// their equivalent URL-encoded hex codes.
function colorCodeToShorterNames(string: string) {
for (const [key, value] of Object.entries(shorterNames)) {
if (value.test(string)) {
string = string.replace(value, key);
}
}
return string;
}
function specialHexEncode(match: string) {
// Browsers tolerate these characters, and they're frequent
switch (match) {
case "%20":
return " ";
case "%3D":
return "=";
case "%3A":
return ":";
case "%2F":
return "/";
default:
return match.toLowerCase(); // compresses better
}
}
function svgToDataUri(svgString: string) {
// Strip the Byte-Order Mark if the SVG has one
// eslint-disable-next-line unicorn/number-literal-case
if (svgString.charCodeAt(0) === 0xfeff) {
svgString = svgString.slice(1);
}
const body = colorCodeToShorterNames(collapseWhitespace(svgString)).replace(
REGEX.quotes,
"'"
);
return "data:image/svg+xml," + dataURIPayload(body);
}
const [baseFontSize, { lineHeight: baseLineHeight }] = defaultTheme.fontSize.base;
const { spacing, borderWidth, borderRadius } = defaultTheme;
type Strategy = "base" | "class";
export default plugin.withOptions<{ strategy?: Strategy }>(
options =>
function ({ addBase, addComponents, theme }) {
const strategy =
options?.strategy === undefined ? ["base", "class"] : [options.strategy];
const rules = [
{
base: [
"[type='text']",
"input:where(:not([type]))",
"[type='email']",
"[type='url']",
"[type='password']",
"[type='number']",
"[type='date']",
"[type='datetime-local']",
"[type='month']",
"[type='search']",
"[type='tel']",
"[type='time']",
"[type='week']",
"[multiple]",
"textarea",
"select",
],
class: [".form-input", ".form-textarea", ".form-select", ".form-multiselect"],
styles: {
appearance: "none",
"background-color": "#fff",
"border-color": theme("colors.gray.500", colors.gray[500]),
"border-width": borderWidth["DEFAULT"],
"border-radius": borderRadius.none,
"padding-top": spacing[2],
"padding-right": spacing[3],
"padding-bottom": spacing[2],
"padding-left": spacing[3],
"font-size": baseFontSize,
"line-height": baseLineHeight,
"--tw-shadow": "0 0 #0000",
"&:focus": {
outline: "2px solid transparent",
"outline-offset": "2px",
"--tw-ring-inset": "var(--tw-empty,/*!*/ /*!*/)",
"--tw-ring-offset-width": "0px",
"--tw-ring-offset-color": "#fff",
"--tw-ring-color": theme("colors.blue.600", colors.blue[600]),
"--tw-ring-offset-shadow": `var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)`,
"--tw-ring-shadow": `var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)`,
"box-shadow": `var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)`,
"border-color": theme("colors.blue.600", colors.blue[600]),
},
},
},
{
base: ["input::placeholder", "textarea::placeholder"],
class: [".form-input::placeholder", ".form-textarea::placeholder"],
styles: {
color: theme("colors.gray.500", colors.gray[500]),
opacity: "1",
},
},
{
base: ["::-webkit-datetime-edit-fields-wrapper"],
class: [".form-input::-webkit-datetime-edit-fields-wrapper"],
styles: {
padding: "0",
},
},
{
// Unfortunate hack until https://bugs.webkit.org/show_bug.cgi?id=198959 is fixed.
// This sucks because users can't change line-height with a utility on date inputs now.
// Reference: https://github.com/twbs/bootstrap/pull/31993
base: ["::-webkit-date-and-time-value"],
class: [".form-input::-webkit-date-and-time-value"],
styles: {
"min-height": "1.5em",
},
},
{
// In Safari on iOS date and time inputs are centered instead of left-aligned and can't be
// changed with `text-align` utilities on the input by default. Resetting this to `inherit`
// makes them left-aligned by default and makes it possible to override the alignment with
// utility classes without using an arbitrary variant to target the pseudo-elements.
base: ["::-webkit-date-and-time-value"],
class: [".form-input::-webkit-date-and-time-value"],
styles: {
"text-align": "inherit",
},
},
{
// In Safari on macOS date time inputs that are set to `display: block` have unexpected
// extra bottom spacing. This can be corrected by setting the `::-webkit-datetime-edit`
// pseudo-element to `display: inline-flex`, instead of the browser default of
// `display: inline-block`.
base: ["::-webkit-datetime-edit"],
class: [".form-input::-webkit-datetime-edit"],
styles: {
display: "inline-flex",
},
},
{
// In Safari on macOS date time inputs are 4px taller than normal inputs
// This is because there is extra padding on the datetime-edit and datetime-edit-{part}-field pseudo elements
// See https://github.com/tailwindlabs/tailwindcss-forms/issues/95
base: [
"::-webkit-datetime-edit",
"::-webkit-datetime-edit-year-field",
"::-webkit-datetime-edit-month-field",
"::-webkit-datetime-edit-day-field",
"::-webkit-datetime-edit-hour-field",
"::-webkit-datetime-edit-minute-field",
"::-webkit-datetime-edit-second-field",
"::-webkit-datetime-edit-millisecond-field",
"::-webkit-datetime-edit-meridiem-field",
],
class: [
".form-input::-webkit-datetime-edit",
".form-input::-webkit-datetime-edit-year-field",
".form-input::-webkit-datetime-edit-month-field",
".form-input::-webkit-datetime-edit-day-field",
".form-input::-webkit-datetime-edit-hour-field",
".form-input::-webkit-datetime-edit-minute-field",
".form-input::-webkit-datetime-edit-second-field",
".form-input::-webkit-datetime-edit-millisecond-field",
".form-input::-webkit-datetime-edit-meridiem-field",
],
styles: {
"padding-top": 0,
"padding-bottom": 0,
},
},
{
base: ["select"],
class: [".form-select"],
styles: {
"background-image": `url("${svgToDataUri(
``
)}")`,
"background-position": `right ${spacing[2]} center`,
"background-repeat": `no-repeat`,
"background-size": `1.5em 1.5em`,
"padding-right": spacing[10],
"print-color-adjust": `exact`,
},
},
{
base: ["[multiple]", '[size]:where(select:not([size="1"]))'],
class: ['.form-select:where([size]:not([size="1"]))'],
styles: {
"background-image": "initial",
"background-position": "initial",
"background-repeat": "unset",
"background-size": "initial",
"padding-right": spacing[3],
"print-color-adjust": "unset",
},
},
{
base: [`[type='checkbox']`, `[type='radio']`],
class: [".form-checkbox", ".form-radio"],
styles: {
appearance: "none",
padding: "0",
"print-color-adjust": "exact",
display: "inline-block",
"vertical-align": "middle",
"background-origin": "border-box",
"user-select": "none",
"flex-shrink": "0",
height: spacing[4],
width: spacing[4],
color: theme("colors.blue.600", colors.blue[600]),
"background-color": "#fff",
"border-color": theme("colors.gray.500", colors.gray[500]),
"border-width": borderWidth["DEFAULT"],
"--tw-shadow": "0 0 #0000",
},
},
{
base: [`[type='checkbox']`],
class: [".form-checkbox"],
styles: {
"border-radius": borderRadius["none"],
},
},
{
base: [`[type='radio']`],
class: [".form-radio"],
styles: {
"border-radius": "100%",
},
},
{
base: [`[type='checkbox']:focus`, `[type='radio']:focus`],
class: [".form-checkbox:focus", ".form-radio:focus"],
styles: {
outline: "2px solid transparent",
"outline-offset": "2px",
"--tw-ring-inset": "var(--tw-empty,/*!*/ /*!*/)",
"--tw-ring-offset-width": "2px",
"--tw-ring-offset-color": "#fff",
"--tw-ring-color": theme("colors.blue.600", colors.blue[600]),
"--tw-ring-offset-shadow": `var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)`,
"--tw-ring-shadow": `var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)`,
"box-shadow": `var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)`,
},
},
{
base: [`[type='checkbox']:checked`, `[type='radio']:checked`],
class: [".form-checkbox:checked", ".form-radio:checked"],
styles: {
"border-color": `transparent`,
"background-color": `currentColor`,
"background-size": `100% 100%`,
"background-position": `center`,
"background-repeat": `no-repeat`,
},
},
{
base: [`[type='checkbox']:checked`],
class: [".form-checkbox:checked"],
styles: {
"background-image": `url("${svgToDataUri(
``
)}")`,
"@media (forced-colors: active) ": {
appearance: "auto",
},
},
},
{
base: [`[type='radio']:checked`],
class: [".form-radio:checked"],
styles: {
"background-image": `url("${svgToDataUri(
``
)}")`,
"@media (forced-colors: active) ": {
appearance: "auto",
},
},
},
{
base: [
`[type='checkbox']:checked:hover`,
`[type='checkbox']:checked:focus`,
`[type='radio']:checked:hover`,
`[type='radio']:checked:focus`,
],
class: [
".form-checkbox:checked:hover",
".form-checkbox:checked:focus",
".form-radio:checked:hover",
".form-radio:checked:focus",
],
styles: {
"border-color": "transparent",
"background-color": "currentColor",
},
},
{
base: [`[type='checkbox']:indeterminate`],
class: [".form-checkbox:indeterminate"],
styles: {
"background-image": `url("${svgToDataUri(
``
)}")`,
"border-color": `transparent`,
"background-color": `currentColor`,
"background-size": `100% 100%`,
"background-position": `center`,
"background-repeat": `no-repeat`,
"@media (forced-colors: active) ": {
appearance: "auto",
},
},
},
{
base: [
`[type='checkbox']:indeterminate:hover`,
`[type='checkbox']:indeterminate:focus`,
],
class: [
".form-checkbox:indeterminate:hover",
".form-checkbox:indeterminate:focus",
],
styles: {
"border-color": "transparent",
"background-color": "currentColor",
},
},
{
base: [`[type='file']`],
class: null,
styles: {
background: "unset",
"border-color": "inherit",
"border-width": "0",
"border-radius": "0",
padding: "0",
"font-size": "unset",
"line-height": "inherit",
},
},
{
base: [`[type='file']:focus`],
class: null,
styles: {
outline: [`1px solid ButtonText`, `1px auto -webkit-focus-ring-color`],
},
},
];
const getStrategyRules = (strategy: Strategy): CSSRuleObject[] =>
rules
.map(rule => {
if (rule[strategy] === null) return null;
return { [rule[strategy].join(", ")]: rule.styles } as CSSRuleObject;
})
.filter(Boolean);
if (strategy.includes("base")) {
addBase(getStrategyRules("base"));
}
if (strategy.includes("class")) {
addComponents(getStrategyRules("class"));
}
}
);