Skip to content

Instantly share code, notes, and snippets.

@srcrip
Created September 21, 2024 15:47
Show Gist options
  • Save srcrip/1a646a31fc64a0c61790dedc54f86980 to your computer and use it in GitHub Desktop.
Save srcrip/1a646a31fc64a0c61790dedc54f86980 to your computer and use it in GitHub Desktop.
This is just a copy-and-paste of the Tailwind Forms plugin that you can actually edit/customize/maintain + JSDoc types.
// Tailwind plugin for forms.
// Basically: https://github.com/tailwindlabs/tailwindcss-forms/blob/main/src/index.js
// But with a few changes. And it's easier to maintain this way.
const svgToDataUri = require("mini-svg-data-uri")
const plugin = require("tailwindcss/plugin")
const defaultTheme = require("tailwindcss/defaultTheme")
const colors = require("tailwindcss/colors")
const [baseFontSize, { lineHeight: baseLineHeight }] =
defaultTheme.fontSize.base
const { spacing, borderWidth, borderRadius } = defaultTheme
/**
* Resolves a color value with an opacity variable.
* @param {string} color - The color value to resolve.
* @param {string} opacityVariableName - The name of the opacity variable.
* @returns {string} The resolved color value.
*/
function resolveColor(color, opacityVariableName) {
return color.replace("<alpha-value>", `var(${opacityVariableName}, 1)`)
}
/**
* Tailwind plugin for customizing form elements.
* @type {import('tailwindcss/plugin')<{strategy?: 'base' | 'class' | Array<'base' | 'class'>}>}
*/
const forms = plugin.withOptions(
(options = { strategy: undefined }) =>
({ addBase, addComponents, theme }) => {
/**
* Resolves the chevron color for select elements.
* @param {string} color - The color to resolve.
* @param {string} fallback - The fallback color.
* @returns {string} The resolved color.
*/
function resolveChevronColor(color, fallback) {
const resolved = theme(color)
if (!resolved || resolved.includes("var(")) {
return fallback
}
return resolved.replace("<alpha-value>", "1")
}
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": resolveColor(
theme("colors.gray.500", colors.gray[500]),
"--tw-border-opacity"
),
"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": resolveColor(
theme("colors.blue.600", colors.blue[600]),
"--tw-ring-opacity"
),
"--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": resolveColor(
theme("colors.blue.600", colors.blue[600]),
"--tw-border-opacity"
)
}
}
},
{
base: ["input::placeholder", "textarea::placeholder"],
class: [".form-input::placeholder", ".form-textarea::placeholder"],
styles: {
color: resolveColor(
theme("colors.gray.500", colors.gray[500]),
"--tw-text-opacity"
),
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(
`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path stroke="${resolveChevronColor(
"colors.gray.500",
colors.gray[500]
)}" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 8l4 4 4-4"/></svg>`
)}")`,
"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: resolveColor(
theme("colors.blue.600", colors.blue[600]),
"--tw-text-opacity"
),
"background-color": "#fff",
"border-color": resolveColor(
theme("colors.gray.500", colors.gray[500]),
"--tw-border-opacity"
),
"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": resolveColor(
theme("colors.blue.600", colors.blue[600]),
"--tw-ring-opacity"
),
"--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(
`<svg viewBox="0 0 16 16" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z"/></svg>`
)}")`,
"@media (forced-colors: active) ": {
appearance: "auto"
}
}
},
{
base: [`[type='radio']:checked`],
class: [".form-radio:checked"],
styles: {
"background-image": `url("${svgToDataUri(
`<svg viewBox="0 0 16 16" fill="white" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="3"/></svg>`
)}")`,
"@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(
`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h8"/></svg>`
)}")`,
"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"
]
}
}
]
/**
* Gets the rules for a specific strategy.
* @param {'base' | 'class'} strategy - The strategy to get rules for.
* @returns {Object[]} An array of rule objects.
*/
const getStrategyRules = strategy =>
rules
.map(rule => {
if (rule[strategy] === null) return null
return { [rule[strategy]]: rule.styles }
})
.filter(Boolean)
if (strategy.includes("base")) {
addBase(getStrategyRules("base"))
}
if (strategy.includes("class")) {
addComponents(getStrategyRules("class"))
}
}
)
module.exports = forms
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment