Skip to content

Instantly share code, notes, and snippets.

@srcrip
Created September 21, 2024 15:47
Show Gist options
  • Select an option

  • Save srcrip/1a646a31fc64a0c61790dedc54f86980 to your computer and use it in GitHub Desktop.

Select an option

Save srcrip/1a646a31fc64a0c61790dedc54f86980 to your computer and use it in GitHub Desktop.

Revisions

  1. srcrip created this gist Sep 21, 2024.
    421 changes: 421 additions & 0 deletions tailwindForms.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,421 @@
    // 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