> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Time Field – Code

> Allows users to enter a time into an input, potentially with predefined stops.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

<Tabs>
  <Tab title="Implementation">
    <LiveCode showCode example="timefield-playground" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { TimeField } from "@servicetitan/anvil2";

      function App() {
        return (
          <TimeField
            label="Label"
            onChange={(change) => {
              console.log("Time changed:", change.time);
            }}
          />
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Common Examples

    ```tsx theme={null}
    import { TimeField } from "@servicetitan/anvil2";

    function ExampleComponent() {
      return (
        <TimeField
          label="Label Text"
          step={60}
          onChange={(change) => {
            // change function
          }}
        />
      );
    }
    ```

    ### With Suggestions Enabled (Default)

    By default, time fields use a 12-hour format and present suggested times in a dropdown based on the `step` prop. The dropdown is only present if there is more than one valid option to select from. Whether the dropdown is present or not, the input allows typing in the field itself to get to your desired time.

    There is autocomplete for best guess assumptions of the time you might be going for based on a partial input value. If both AM and PM times both are possible options, AM takes precedence over PM for autocomplete.

    <LiveCode showCode example="timefield-playground" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { TimeField } from "@servicetitan/anvil2";

      function App() {
        return (
          <TimeField
            label="Label"
            onChange={(change) => {
              console.log("Time changed:", change.time);
            }}
          />
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Without Suggestions

    When `disableSuggestions` is set to true, the suggestions dropdown will no longer appear. All other features, including autocomplete, will continue to work as the user types.

    <LiveCode showCode example="timefield-disablesuggestions" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { TimeField } from "@servicetitan/anvil2";

      function App() {
        return (
          <TimeField
            label="Field without suggestions"
            disableSuggestions
            onChange={(change) => {
              console.log("Time changed:", change.time);
            }}
          />
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Auto-Rounding

    If the `autoround` prop is set to true, the time field will attempt to round to the next valid time on blur based on the `step` and `max` props. Note: rounding will always go to the future, never the past.

    If suggestions are enabled, the suggestions will display the auto rounded options available, even if the time currently typed in the input is not yet rounded.

    Add a `description` to allow users to know auto rounding is enabled for the field.

    <LiveCode showCode example="timefield-autoround" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { TimeField } from "@servicetitan/anvil2";

      function App() {
        return (
          <TimeField
            label="Autorounding field"
            autoround
            description="Autorounding is enabled for this field"
            onChange={(change) => {
              console.log("Time changed:", change.time);
            }}
          />
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Min and Max Constraints

    Use the `min` and `max` props to set a time frame constraint. When the props are present, a time outside the time frame can not be entered into the field.

    Autocomplete will adjust to a time within the time frame if the partial value can fall within the constraint. For example, 02:-- will autocomplete to 02:00 PM for a 09:00 AM - 05:00 PM time frame instead of the default 02:00 AM since 02:00 AM does not fall between 09:00 AM and 05:00 PM.

    `autoround` will cap at the max time even if it doesn't fully align with the step prop. For example: In a field with a 09:00 AM - 05:00 PM time frame and a step of 30 minutes, if 4:45 is entered it will round to 5:00 PM.

    Add a `description` to allow users to know there is a time constraint.

    <LiveCode showCode example="timefield-minmax" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { TimeField, Grid } from "@servicetitan/anvil2";

      function App() {
        return (
          <Grid templateColumns="repeat(2, 1fr)" gap={4}>
            <TimeField
              label="Field with min/max"
              min="09:00 AM"
              max="05:00 PM"
              description="Enter a time between 09:00 AM and 05:00 PM"
              onChange={(change) => {
                console.log("Time changed:", change.time);
              }}
            />

            <TimeField
              label="Field with min/max/autoround"
              min="09:00 AM"
              max="05:00 PM"
              autoround
              description="Enter a time between 09:00 AM and 05:00 PM. Autorounding is enabled."
              onChange={(change) => {
                console.log("Time changed:", change.time);
              }}
            />
          </Grid>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### 24 Hour Format

    Set the `format` prop to 24 to change to the time format. When using this format, ensure `min` and `max` times are also set to 24 hour format.

    <LiveCode showCode example="timefield-format-24" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { TimeField, Grid } from "@servicetitan/anvil2";

      function App() {
        return (
          <Grid templateColumns="repeat(2, 1fr)" gap={4}>
            <TimeField
              label="Field with 24 hour format"
              format={24}
              onChange={(change) => {
                console.log("Time changed:", change.time);
              }}
            />

            <TimeField
              label="Field with 24 hour format/min/max"
              format={24}
              min="09:00"
              max="17:00"
              description="Please enter a time between 09:00 AM and 17:00 PM"
              onChange={(change) => {
                console.log("Time changed:", change.time);
              }}
            />
          </Grid>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Validation

    Validation needs to be handled with the onChange. `isInputValid` and `isInputEmpty` are boolean props returned with the time so that custom validation can be applied. Note: Because of the use of the mask acts as a value for the input, native html validation will not occur.

    <LiveCode showCode example="timefield-isinputempty" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { TimeField } from "@servicetitan/anvil2";
      import { useState } from "react";

      function App() {
        const [, setTime] = useState<string | null>(null);
        const [error, setError] = useState<string>("");

        return (
          <TimeField
            label="Required field"
            required
            error={error}
            onChange={(change) => {
              setTime(change.time);

              if (change.isInputEmpty) {
                setError("Time is required");
              } else {
                setError("");
              }
            }}
          />
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Markdown in labels

    The `label` prop supports inline markdown: bold (`**text**`), italic (`*text*`), bold and italic (`***text***`), highlight (`==text==`), and code (`` `text` ``).

    <LiveCode showCode example="timefield-markdownlabel" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { Flex, TimeField } from "@servicetitan/anvil2";

      function App() {
        return (
          <Flex direction="column" gap="4" style={{ maxWidth: 400 }}>
            <TimeField label="**Bold** label" />
            <TimeField label="*Italic* label" />
            <TimeField label="***Bold and italic*** label" />
            <TimeField label="==Highlight== label" />
            <TimeField label="`Code` label" />
          </Flex>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Hide the label

    Use `hideLabel` to visually hide the label. The `label` string is converted to an `aria-label` on the input so it remains accessible to screen readers — any inline markdown is stripped to plain text.

    <LiveCode showCode example="timefield-hidelabel" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { TimeField } from "@servicetitan/anvil2";

      function App() {
        return <TimeField label="Start time" hideLabel />;
      }

      export default App;
      ```
    </LiveCode>
  </Tab>

  <Tab title="TimeField Props">
    ```tsx theme={null}
    <TimeField
      label="Label Text"
      autoround={false}
      defaultValue="09:00 AM"
      disableSuggestions={false}
      format="12"
      max="05:00 PM"
      min="09:00 AM"
      step={30}
      value="10:30 AM"
      onChange={(change) => console.log(change)}
    />
    ```

    ## `TimeField` Props

    In addition to the props listed below, the `TimeField` accepts most [TextField](/docs/web/components/text-field/design) props with the exception of `type`, `placeholder`, `showCounter` and `maxLength`. This includes `label` (supports inline markdown; omitting is deprecated) and `hideLabel`.

    <ParamField path="autoround" type="boolean" default="false">
      If true, value will round to nearest future step. If there is a defined time
      frame, the ceiling for rounding is the max time.
    </ParamField>

    <ParamField path="defaultValue" type="string | null">
      Uncontrolled initial value for the component.
    </ParamField>

    <ParamField path="disableSuggestions" type="boolean" default="false">
      If true, the dropdown with suggested time options is not present.
    </ParamField>

    <ParamField path="format" type={`"12" | "24"`} default="12">
      Time format for the component.
    </ParamField>

    <ParamField path="labelProps" type={`Omit<FieldLabelProps, "children" | "id" | "htmlFor">`}>
      Additional props passed to the [FieldLabel](/docs/web/components/field-label/code) component. Supports the `aiMark` prop for displaying [AI-powered field indicators](/docs/web/utilities/ai-marks).
    </ParamField>

    <ParamField path="max" type="string">
      Maximum allowed time for a time frame. Must be paired with `min`. String must
      match the format set for the component.
    </ParamField>

    <ParamField path="min" type="string">
      Minimum allowed time for a time frame. Must be paired with `max`. String must
      match format set for the component.
    </ParamField>

    <ParamField path="onChange" type="(change: TimeFieldChange) => void">
      Callback fired on blur or after a time is selected from the selection list.
    </ParamField>

    <ParamField path="step" type="number" default="30">
      Time increments for the time suggestions and auto rounding.
    </ParamField>

    <ParamField path="value" type="string | null">
      Controlled value for the component.
    </ParamField>
  </Tab>

  <Tab title="TimeFieldChange Props">
    ```tsx theme={null}
    // TimeFieldChange is returned by the onChange callback
    const handleChange = (change: TimeFieldChange) => {
      console.log(change.isInputEmpty, change.isInputValid, change.time);
    };
    ```

    ## `TimeFieldChange` Props

    The onChange callback receives a TimeFieldChange object with the following properties:

    <ParamField path="isInputEmpty" type="boolean">
      Whether the input is empty.
    </ParamField>

    <ParamField path="isInputValid" type="boolean">
      Whether there is a validation issue aside from empty value with the input.
    </ParamField>

    <ParamField path="time" type="string | null">
      The returned time string.
    </ParamField>
  </Tab>
</Tabs>
