Repository: zeit-ui/react
Pull request #367 (2020-08-14 08:55:50)

chore: release v1.8.0
## Checklist

- [x] Fix linting errors
- [x] Tests have been added / updated (or snapshots)

## Change information



codecov[bot]: # [Codecov](https://codecov.io/gh/zeit-ui/react/pull/367?src=pr&el=h1) Report
> Merging [#367](https://codecov.io/gh/zeit-ui/react/pull/367?src=pr&el=desc) into [master](https://codecov.io/gh/zeit-ui/react/commit/229887b3cb8dc8bd90b626e8f8518f0cc23fbcf1&el=desc) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/zeit-ui/react/pull/367/graphs/tree.svg?width=650&height=150&src=pr&token=209SV0GGAX)](https://codecov.io/gh/zeit-ui/react/pull/367?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master     #367   +/-   ##
=======================================
  Coverage   99.95%   99.95%           
=======================================
  Files         186      186           
  Lines        2373     2421   +48     
  Branches      568      588   +20     
=======================================
+ Hits         2372     2420   +48     
  Misses          1        1           
```


| [Impacted Files](https://codecov.io/gh/zeit-ui/react/pull/367?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [components/button/button.tsx](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy9idXR0b24vYnV0dG9uLnRzeA==) | `100.00% <ø> (ø)` | |
| [components/description/description.tsx](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy9kZXNjcmlwdGlvbi9kZXNjcmlwdGlvbi50c3g=) | `100.00% <ø> (ø)` | |
| [components/modal/modal-actions.tsx](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy9tb2RhbC9tb2RhbC1hY3Rpb25zLnRzeA==) | `100.00% <ø> (ø)` | |
| [components/modal/modal-content.tsx](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy9tb2RhbC9tb2RhbC1jb250ZW50LnRzeA==) | `100.00% <ø> (ø)` | |
| [components/popover/popover-item.tsx](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy9wb3BvdmVyL3BvcG92ZXItaXRlbS50c3g=) | `100.00% <ø> (ø)` | |
| [components/select/select-dropdown.tsx](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy9zZWxlY3Qvc2VsZWN0LWRyb3Bkb3duLnRzeA==) | `100.00% <ø> (ø)` | |
| [components/select/select.tsx](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy9zZWxlY3Qvc2VsZWN0LnRzeA==) | `100.00% <ø> (ø)` | |
| [components/table/table-context.ts](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy90YWJsZS90YWJsZS1jb250ZXh0LnRz) | `100.00% <ø> (ø)` | |
| [components/tabs/tabs-context.ts](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy90YWJzL3RhYnMtY29udGV4dC50cw==) | `100.00% <ø> (ø)` | |
| [components/button/button-icon.tsx](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree#diff-Y29tcG9uZW50cy9idXR0b24vYnV0dG9uLWljb24udHN4) | `100.00% <100.00%> (ø)` | |
| ... and [13 more](https://codecov.io/gh/zeit-ui/react/pull/367/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/zeit-ui/react/pull/367?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/zeit-ui/react/pull/367?src=pr&el=footer). Last update [229887b...ee2cdd4](https://codecov.io/gh/zeit-ui/react/pull/367?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/components/button/button-icon.tsx b/components/button/button-icon.tsx
index f4c6b48..bca390a 100644
--- a/components/button/button-icon.tsx
+++ b/components/button/button-icon.tsx
@@ -3,6 +3,7 @@ import withDefaults from '../utils/with-defaults'
 
 interface Props {
   isRight?: boolean
+  isSingle?: boolean
   className?: string
 }
 
@@ -16,12 +17,15 @@ export type ButtonIconProps = Props & typeof defaultProps & NativeAttrs
 
 const ButtonIcon: React.FC<React.PropsWithChildren<ButtonIconProps>> = ({
   isRight,
+  isSingle,
   children,
   className,
   ...props
 }) => {
   return (
-    <span className={`icon ${isRight ? 'right' : ''} ${className}`} {...props}>
+    <span
+      className={`icon ${isRight ? 'right' : ''} ${isSingle ? 'single' : ''} ${className}`}
+      {...props}>
       {children}
       <style jsx>{`
         .icon {
@@ -47,6 +51,11 @@ const ButtonIcon: React.FC<React.PropsWithChildren<ButtonIconProps>> = ({
           height: calc(var(--zeit-ui-button-height) / 2.35);
           width: calc(var(--zeit-ui-button-height) / 2.35);
         }
+
+        .single {
+          position: static;
+          transform: none;
+        }
       `}</style>
     </span>
   )
diff --git a/components/button/button.tsx b/components/button/button.tsx
index b2e3cd0..9da127d 100644
--- a/components/button/button.tsx
+++ b/components/button/button.tsx
@@ -64,6 +64,7 @@ const Button = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<Butto
     const [dripY, setDripY] = useState<number>(0)
     const groupConfig = useButtonGroupContext()
     const filteredProps = filterPropsWithGroup(btnProps, groupConfig)
+    /* eslint-disable @typescript-eslint/no-unused-vars */
     const {
       children,
       disabled,
@@ -81,6 +82,7 @@ const Button = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<Butto
       className,
       ...props
     } = filteredProps
+    /* eslint-enable @typescript-eslint/no-unused-vars */
 
     const { bg, border, color } = useMemo(() => getButtonColors(theme.palette, filteredProps), [
       theme.palette,
@@ -180,7 +182,8 @@ const Button = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<Butto
             --zeit-ui-button-bg: ${bg};
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
             color: ${hover.color};
             --zeit-ui-button-color: ${hover.color};
             background-color: ${hover.bg};
diff --git a/components/button/utils.tsx b/components/button/utils.tsx
index 9df5c29..dfbd3cb 100644
--- a/components/button/utils.tsx
+++ b/components/button/utils.tsx
@@ -21,6 +21,13 @@ export const getButtonChildrenWithIcon = (
       ? `calc(var(--zeit-ui-button-height) / 2 + var(--zeit-ui-button-padding) * .5)`
       : 0
   if (!hasIcon) return <div className="text">{children}</div>
+  if (React.Children.count(children) === 0) {
+    return (
+      <ButtonIcon isRight={isRight} isSingle>
+        {hasIcon}
+      </ButtonIcon>
+    )
+  }
   return (
     <>
       <ButtonIcon isRight={isRight}>{hasIcon}</ButtonIcon>
diff --git a/components/description/description.tsx b/components/description/description.tsx
index 30c000d..0aa18b5 100644
--- a/components/description/description.tsx
+++ b/components/description/description.tsx
@@ -37,6 +37,7 @@ const Description: React.FC<DescriptionProps> = ({ title, content, className, ..
           text-transform: uppercase;
           white-space: nowrap;
           color: ${theme.palette.accents_5};
+          font-weight: 500;
           display: flex;
         }
 
diff --git a/components/input/input.tsx b/components/input/input.tsx
index a37da1d..d907514 100644
--- a/components/input/input.tsx
+++ b/components/input/input.tsx
@@ -67,7 +67,7 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
     const [selfValue, setSelfValue] = useState<string>(initialValue)
     const [hover, setHover] = useState<boolean>(false)
     const { heightRatio, fontSize } = useMemo(() => getSizes(size), [size])
-    const showClearIcon = useMemo(() => clearable && selfValue !== '', [selfValue, clearable])
+    const isControlledComponent = useMemo(() => value !== undefined, [value])
     const labelClasses = useMemo(() => (labelRight ? 'right-label' : label ? 'left-label' : ''), [
       label,
       labelRight,
@@ -80,12 +80,12 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
       theme.palette,
       status,
     ])
+
     const changeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
       if (disabled || readOnly) return
       setSelfValue(event.target.value)
       onChange && onChange(event)
     }
-
     const clearHandler = (event: React.MouseEvent<HTMLDivElement>) => {
       setSelfValue('')
       onClearClick && onClearClick(event)
@@ -121,9 +121,18 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
     )
 
     useEffect(() => {
-      if (value === undefined) return
-      setSelfValue(value)
-    }, [value])
+      if (isControlledComponent) {
+        setSelfValue(value as string)
+      }
+    })
+
+    const controlledValue = isControlledComponent
+      ? { value: selfValue }
+      : { defaultValue: initialValue }
+    const inputProps = {
+      ...props,
+      ...controlledValue,
+    }
 
     return (
       <div className="with-label">
@@ -139,7 +148,6 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
               type="text"
               ref={inputRef}
               className={`${disabled ? 'disabled' : ''} ${iconClasses}`}
-              value={selfValue}
               placeholder={placeholder}
               disabled={disabled}
               readOnly={readOnly}
@@ -147,11 +155,11 @@ const Input = React.forwardRef<HTMLInputElement, React.PropsWithChildren<InputPr
               onBlur={blurHandler}
               onChange={changeHandler}
               autoComplete={autoComplete}
-              {...props}
+              {...inputProps}
             />
             {clearable && (
               <InputClearIcon
-                visible={showClearIcon}
+                visible={Boolean(inputRef.current && inputRef.current.value !== '')}
                 heightRatio={heightRatio}
                 disabled={disabled || readOnly}
                 onClick={clearHandler}
diff --git a/components/link/link.tsx b/components/link/link.tsx
index 887f377..4ce1ca1 100644
--- a/components/link/link.tsx
+++ b/components/link/link.tsx
@@ -3,6 +3,7 @@ import withDefaults from '../utils/with-defaults'
 import useTheme from '../styles/use-theme'
 import useWarning from '../utils/use-warning'
 import LinkIcon from './icon'
+import { addColorAlpha } from '../utils/color'
 
 export interface Props {
   href?: string
@@ -65,7 +66,7 @@ const Link = React.forwardRef<HTMLAnchorElement, React.PropsWithChildren<LinkPro
           }
 
           .link:hover {
-            background-color: ${block ? '#0076ff1a' : 'unset'};
+            background-color: ${block ? addColorAlpha(theme.palette.link, 0.1) : 'unset'};
             color: ${hoverColor};
           }
         `}</style>
diff --git a/components/modal/modal-action.tsx b/components/modal/modal-action.tsx
index 60e5f24..ce06339 100644
--- a/components/modal/modal-action.tsx
+++ b/components/modal/modal-action.tsx
@@ -1,7 +1,15 @@
-import React, { MouseEvent, useMemo } from 'react'
-import withDefaults from '../utils/with-defaults'
+import React, {
+  MouseEvent,
+  PropsWithoutRef,
+  RefAttributes,
+  useImperativeHandle,
+  useMemo,
+  useRef,
+} from 'react'
+import css from 'styled-jsx/css'
 import useTheme from '../styles/use-theme'
 import { useModalContext } from './modal-context'
+import Button, { ButtonProps } from '../button/button'
 
 type ModalActionEvent = MouseEvent<HTMLButtonElement> & {
   close: () => void
@@ -20,66 +28,83 @@ const defaultProps = {
   disabled: false,
 }
 
-type NativeAttrs = Omit<React.ButtonHTMLAttributes<any>, keyof Props>
-export type ModalActionProps = Props & typeof defaultProps & NativeAttrs
+export type ModalActionProps = Props & typeof defaultProps & Omit<ButtonProps, keyof Props>
 
-const ModalAction: React.FC<ModalActionProps> = ({
-  className,
-  children,
-  onClick,
-  passive,
-  disabled,
-  ...props
-}) => {
-  const theme = useTheme()
-  const { close } = useModalContext()
-  const clickHandler = (event: MouseEvent<HTMLButtonElement>) => {
-    if (disabled) return
-    const actionEvent = Object.assign({}, event, {
-      close: () => close && close(),
-    })
-    onClick && onClick(actionEvent)
-  }
+const ModalAction = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<ModalActionProps>>(
+  (
+    { className, children, onClick, passive, disabled, ...props },
+    ref: React.Ref<HTMLButtonElement | null>,
+  ) => {
+    const theme = useTheme()
+    const btnRef = useRef<HTMLButtonElement>(null)
+    const { close } = useModalContext()
+    useImperativeHandle(ref, () => btnRef.current)
 
-  const color = useMemo(() => {
-    return passive || disabled ? theme.palette.accents_5 : theme.palette.foreground
-  }, [theme.palette, passive, disabled])
+    const clickHandler = (event: MouseEvent<HTMLButtonElement>) => {
+      if (disabled) return
+      const actionEvent = Object.assign({}, event, {
+        close: () => close && close(),
+      })
+      onClick && onClick(actionEvent)
+    }
 
-  const bgColor = useMemo(() => {
-    return disabled ? theme.palette.accents_1 : theme.palette.background
-  }, [theme.palette, disabled])
+    const color = useMemo(() => {
+      return passive ? theme.palette.accents_5 : theme.palette.foreground
+    }, [theme.palette, passive, disabled])
 
-  return (
-    <>
-      <button className={className} onClick={clickHandler} {...props}>
+    const bgColor = useMemo(() => {
+      return disabled ? theme.palette.accents_1 : theme.palette.background
+    }, [theme.palette, disabled])
+
+    const { className: resolveClassName, styles } = css.resolve`
+      button.btn {
+        font-size: 0.75rem;
+        border: none;
+        color: ${color};
+        background-color: ${theme.palette.background};
+        display: flex;
+        -webkit-box-align: center;
+        align-items: center;
+        -webkit-box-pack: center;
+        justify-content: center;
+        flex: 1;
+        height: 100%;
+        border-radius: 0;
+      }
+      button.btn:hover,
+      button.btn:focus {
+        color: ${disabled ? color : theme.palette.foreground};
+        background-color: ${disabled ? bgColor : theme.palette.accents_1};
+      }
+    `
+
+    const overrideProps = {
+      ...props,
+      effect: false,
+      ref: btnRef,
+    }
+
+    return (
+      <Button
+        className={`${resolveClassName} ${className}`}
+        onClick={clickHandler}
+        disabled={disabled}
+        {...overrideProps}>
         {children}
-      </button>
-      <style jsx>{`
-        button {
-          font-size: 0.75rem;
-          text-transform: uppercase;
-          display: flex;
-          -webkit-box-align: center;
-          align-items: center;
-          -webkit-box-pack: center;
-          justify-content: center;
-          outline: none;
-          text-decoration: none;
-          transition: all 200ms ease-in-out 0s;
-          border: none;
-          color: ${color};
-          background-color: ${bgColor};
-          cursor: ${disabled ? 'not-allowed' : 'pointer'};
-          flex: 1;
-        }
+        {styles}
+      </Button>
+    )
+  },
+)
 
-        button:hover {
-          color: ${disabled ? color : theme.palette.foreground};
-          background-color: ${disabled ? bgColor : theme.palette.accents_1};
-        }
-      `}</style>
-    </>
-  )
-}
+type ModalActionComponent<T, P = {}> = React.ForwardRefExoticComponent<
+  PropsWithoutRef<P> & RefAttributes<T>
+>
+
+type ComponentProps = Partial<typeof defaultProps> &
+  Omit<Props, keyof typeof defaultProps> &
+  Partial<Omit<ButtonProps, keyof Props>>
+
+ModalAction.defaultProps = defaultProps
 
-export default withDefaults(ModalAction, defaultProps)
+export default ModalAction as ModalActionComponent<HTMLButtonElement, ComponentProps>
diff --git a/components/modal/modal-actions.tsx b/components/modal/modal-actions.tsx
index 0c2d088..443c3ac 100644
--- a/components/modal/modal-actions.tsx
+++ b/components/modal/modal-actions.tsx
@@ -22,7 +22,7 @@ const ModalActions: React.FC<React.PropsWithChildren<{}>> = ({ children, ...prop
           border-bottom-right-radius: ${theme.layout.radius};
         }
 
-        footer > :global(button + button) {
+        footer > :global(button.btn + button.btn) {
           border-left: 1px solid ${theme.palette.border};
         }
 
diff --git a/components/modal/modal-content.tsx b/components/modal/modal-content.tsx
index ac7c45b..43561bf 100644
--- a/components/modal/modal-content.tsx
+++ b/components/modal/modal-content.tsx
@@ -26,6 +26,7 @@ const ModalContent: React.FC<ModalContentProps> = ({ className, children, ...pro
           margin: 0 -${theme.layout.gap};
           padding: ${theme.layout.gap} ${theme.layout.gap} ${theme.layout.gapHalf};
           overflow-y: auto;
+          position: relative;
         }
 
         .content > :global(*:first-child) {
diff --git a/components/modal/modal-wrapper.tsx b/components/modal/modal-wrapper.tsx
index 568f81e..35e79b3 100644
--- a/components/modal/modal-wrapper.tsx
+++ b/components/modal/modal-wrapper.tsx
@@ -1,7 +1,8 @@
-import React from 'react'
+import React, { useEffect, useRef } from 'react'
 import withDefaults from '../utils/with-defaults'
 import useTheme from '../styles/use-theme'
 import CSSTransition from '../shared/css-transition'
+import { isChildElement } from '../utils/collections'
 
 interface Props {
   className?: string
@@ -24,15 +25,49 @@ const ModalWrapper: React.FC<React.PropsWithChildren<ModalWrapperProps>> = ({
   ...props
 }) => {
   const theme = useTheme()
+  const modalContent = useRef<HTMLDivElement>(null)
+  const tabStart = useRef<HTMLDivElement>(null)
+  const tabEnd = useRef<HTMLDivElement>(null)
+
+  useEffect(() => {
+    if (!visible) return
+    const activeElement = document.activeElement
+    const isChild = isChildElement(modalContent.current, activeElement)
+    if (isChild) return
+    tabStart.current && tabStart.current.focus()
+  }, [visible])
+
+  const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
+    const isTabDown = event.keyCode === 9
+    if (!visible || !isTabDown) return
+    const activeElement = document.activeElement
+    if (event.shiftKey) {
+      if (activeElement === tabStart.current) {
+        tabEnd.current && tabEnd.current.focus()
+      }
+    } else {
+      if (activeElement === tabEnd.current) {
+        tabStart.current && tabStart.current.focus()
+      }
+    }
+  }
 
   return (
     <CSSTransition name="wrapper" visible={visible} clearTime={300}>
-      <div className={`wrapper ${className}`} {...props}>
+      <div
+        className={`wrapper ${className}`}
+        role="dialog"
+        tabIndex={-1}
+        onKeyDown={onKeyDown}
+        ref={modalContent}
+        {...props}>
+        <div tabIndex={0} className="hide-tab" aria-hidden="true" ref={tabStart} />
         {children}
+        <div tabIndex={0} className="hide-tab" aria-hidden="true" ref={tabEnd} />
         <style jsx>{`
           .wrapper {
-            max-width: 85vw;
-            max-height: 75vh;
+            max-width: 90vw;
+            max-height: 90vh;
             width: ${width};
             overflow: hidden;
             display: flex;
@@ -45,14 +80,15 @@ const ModalWrapper: React.FC<React.PropsWithChildren<ModalWrapperProps>> = ({
             padding: ${theme.layout.gap};
             box-shadow: ${theme.expressiveness.shadowLarge};
             opacity: 0;
-            transform: translate3d(0px, -40px, 0px);
+            outline: none;
+            transform: translate3d(0px, -30px, 0px);
             transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s,
               transform 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
           }
 
           .wrapper-enter {
             opacity: 0;
-            transform: translate3d(0px, -40px, 0px);
+            transform: translate3d(0px, -30px, 0px);
           }
 
           .wrapper-enter-active {
@@ -67,7 +103,15 @@ const ModalWrapper: React.FC<React.PropsWithChildren<ModalWrapperProps>> = ({
 
           .wrapper-leave-active {
             opacity: 0;
-            transform: translate3d(0px, -50px, 0px);
+            transform: translate3d(0px, -30px, 0px);
+          }
+
+          .hide-tab {
+            outline: none;
+            overflow: hidden;
+            width: 0;
+            height: 0;
+            opacity: 0;
           }
         `}</style>
       </div>
diff --git a/components/popover/popover-item.tsx b/components/popover/popover-item.tsx
index 21c5c5d..ba13aac 100644
--- a/components/popover/popover-item.tsx
+++ b/components/popover/popover-item.tsx
@@ -39,7 +39,6 @@ const PopoverItem: React.FC<React.PropsWithChildren<PopoverItemProps>> = ({
             line-height: 1.25rem;
             text-align: left;
             transition: color 0.1s ease 0s, background-color 0.1s ease 0s;
-            width: max-content;
           }
 
           .item:hover {
@@ -50,6 +49,12 @@ const PopoverItem: React.FC<React.PropsWithChildren<PopoverItemProps>> = ({
             margin: 0;
           }
 
+          .item > :global(.link) {
+            width: 100%;
+            padding: 0.5rem ${theme.layout.gap};
+            margin: -0.5rem -${theme.layout.gap};
+          }
+
           .item.line {
             line-height: 0;
             height: 0;
diff --git a/components/select/select-dropdown.tsx b/components/select/select-dropdown.tsx
index 25358e0..356451c 100644
--- a/components/select/select-dropdown.tsx
+++ b/components/select/select-dropdown.tsx
@@ -9,6 +9,7 @@ interface Props {
   className?: string
   dropdownStyle?: object
   disableMatchWidth?: boolean
+  getPopupContainer?: () => HTMLElement | null
 }
 
 const defaultProps = {
@@ -25,12 +26,17 @@ const SelectDropdown: React.FC<React.PropsWithChildren<SelectDropdownProps>> = (
   className,
   dropdownStyle,
   disableMatchWidth,
+  getPopupContainer,
 }) => {
   const theme = useTheme()
   const { ref } = useSelectContext()
 
   return (
-    <Dropdown parent={ref} visible={visible} disableMatchWidth={disableMatchWidth}>
+    <Dropdown
+      parent={ref}
+      visible={visible}
+      disableMatchWidth={disableMatchWidth}
+      getPopupContainer={getPopupContainer}>
       <div className={`select-dropdown ${className}`} style={dropdownStyle}>
         {children}
         <style jsx>{`
diff --git a/components/select/select.tsx b/components/select/select.tsx
index b9e6306..2b54fd0 100644
--- a/components/select/select.tsx
+++ b/components/select/select.tsx
@@ -28,6 +28,7 @@ interface Props {
   dropdownClassName?: string
   dropdownStyle?: object
   disableMatchWidth?: boolean
+  getPopupContainer?: () => HTMLElement | null
 }
 
 const defaultProps = {
@@ -60,6 +61,7 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
   dropdownClassName,
   dropdownStyle,
   disableMatchWidth,
+  getPopupContainer,
   ...props
 }) => {
   const theme = useTheme()
@@ -148,7 +150,8 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
           visible={visible}
           className={dropdownClassName}
           dropdownStyle={dropdownStyle}
-          disableMatchWidth={disableMatchWidth}>
+          disableMatchWidth={disableMatchWidth}
+          getPopupContainer={getPopupContainer}>
           {children}
         </SelectDropdown>
         {!pure && (
diff --git a/components/shared/backdrop.tsx b/components/shared/backdrop.tsx
index eb2fc80..425f1b9 100644
--- a/components/shared/backdrop.tsx
+++ b/components/shared/backdrop.tsx
@@ -12,13 +12,13 @@ interface Props {
 const defaultProps = {
   onClick: () => {},
   visible: false,
-  offsetY: 0,
 }
 
-export type BackdropProps = Props & typeof defaultProps
+type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
+export type BackdropProps = Props & typeof defaultProps & NativeAttrs
 
 const Backdrop: React.FC<React.PropsWithChildren<BackdropProps>> = React.memo(
-  ({ children, onClick, visible }) => {
+  ({ children, onClick, visible, ...props }) => {
     const theme = useTheme()
     const [, setIsContentMouseDown, IsContentMouseDownRef] = useCurrentState(false)
     const clickHandler = (event: MouseEvent<HTMLElement>) => {
@@ -37,8 +37,8 @@ const Backdrop: React.FC<React.PropsWithChildren<BackdropProps>> = React.memo(
     }
 
     return (
-      <CSSTransition visible={visible} clearTime={300}>
-        <div className="backdrop" onClick={clickHandler} onMouseUp={mouseUpHandler}>
+      <CSSTransition name="backdrop-wrapper" visible={visible} clearTime={300}>
+        <div className="backdrop" onClick={clickHandler} onMouseUp={mouseUpHandler} {...props}>
           <div className="layer" />
           <div
             onClick={childrenClickHandler}
@@ -56,7 +56,7 @@ const Backdrop: React.FC<React.PropsWithChildren<BackdropProps>> = React.memo(
               align-content: center;
               align-items: center;
               flex-direction: column;
-              justify-content: space-around;
+              justify-content: center;
               height: 100vh;
               width: 100vw;
               overflow: auto;
@@ -93,6 +93,22 @@ const Backdrop: React.FC<React.PropsWithChildren<BackdropProps>> = React.memo(
               pointer-events: none;
               z-index: 1000;
             }
+
+            .backdrop-wrapper-enter .layer {
+              opacity: 0;
+            }
+
+            .backdrop-wrapper-enter-active .layer {
+              opacity: ${theme.expressiveness.portalOpacity};
+            }
+
+            .backdrop-wrapper-leave .layer {
+              opacity: ${theme.expressiveness.portalOpacity};
+            }
+
+            .backdrop-wrapper-leave-active .layer {
+              opacity: 0;
+            }
           `}</style>
         </div>
       </CSSTransition>
diff --git a/components/shared/dropdown.tsx b/components/shared/dropdown.tsx
index 6b6ecb0..50b60c5 100644
--- a/components/shared/dropdown.tsx
+++ b/components/shared/dropdown.tsx
@@ -10,6 +10,7 @@ interface Props {
   parent?: MutableRefObject<HTMLElement | null> | undefined
   visible: boolean
   disableMatchWidth?: boolean
+  getPopupContainer?: () => HTMLElement | null
 }
 
 interface ReactiveDomReact {
@@ -26,31 +27,48 @@ const defaultRect: ReactiveDomReact = {
   width: 0,
 }
 
-const getRect = (ref: MutableRefObject<HTMLElement | null>): ReactiveDomReact => {
+const getOffset = (el?: HTMLElement | null | undefined) => {
+  if (!el)
+    return {
+      top: 0,
+      left: 0,
+    }
+  const { top, left } = el.getBoundingClientRect()
+  return { top, left }
+}
+
+const getRect = (
+  ref: MutableRefObject<HTMLElement | null>,
+  getContainer?: () => HTMLElement | null,
+): ReactiveDomReact => {
   if (!ref || !ref.current) return defaultRect
   const rect = ref.current.getBoundingClientRect()
+  const container = getContainer ? getContainer() : null
+  const scrollElement = container || document.documentElement
+  const { top: offsetTop, left: offsetLeft } = getOffset(container)
+
   return {
     ...rect,
     width: rect.width || rect.right - rect.left,
-    top: rect.bottom + document.documentElement.scrollTop,
-    left: rect.left + document.documentElement.scrollLeft,
+    top: rect.bottom + scrollElement.scrollTop - offsetTop,
+    left: rect.left + scrollElement.scrollLeft - offsetLeft,
   }
 }
 
 const Dropdown: React.FC<React.PropsWithChildren<Props>> = React.memo(
-  ({ children, parent, visible, disableMatchWidth }) => {
-    const el = usePortal('dropdown')
+  ({ children, parent, visible, disableMatchWidth, getPopupContainer }) => {
+    const el = usePortal('dropdown', getPopupContainer)
     const [rect, setRect] = useState<ReactiveDomReact>(defaultRect)
     if (!parent) return null
 
     const updateRect = () => {
-      const { top, left, right, width: nativeWidth } = getRect(parent)
+      const { top, left, right, width: nativeWidth } = getRect(parent, getPopupContainer)
       setRect({ top, left, right, width: nativeWidth })
     }
 
     useResize(updateRect)
     useClickAnyWhere(() => {
-      const { top, left } = getRect(parent)
+      const { top, left } = getRect(parent, getPopupContainer)
       const shouldUpdatePosition = top !== rect.top || left !== rect.left
       if (!shouldUpdatePosition) return
       updateRect()
diff --git a/components/snippet/snippet.tsx b/components/snippet/snippet.tsx
index b7dda7b..232ad95 100644
--- a/components/snippet/snippet.tsx
+++ b/components/snippet/snippet.tsx
@@ -1,7 +1,7 @@
 import React, { useMemo, useRef } from 'react'
 import useTheme from '../styles/use-theme'
 import withDefaults from '../utils/with-defaults'
-import { SnippetTypes, CopyTypes } from '../utils/prop-types'
+import { SnippetTypes, CopyTypes, NormalTypes } from '../utils/prop-types'
 import { getStyles } from './styles'
 import SnippetIcon from './snippet-icon'
 import useClipboard from '../utils/use-clipboard'
@@ -9,6 +9,9 @@ import useToasts from '../use-toasts'
 
 interface Props {
   text?: string | string[]
+  symbol?: string
+  toastText?: string
+  toastType?: NormalTypes
   filled?: boolean
   width?: string
   copy?: CopyTypes
@@ -18,6 +21,9 @@ interface Props {
 
 const defaultProps = {
   filled: false,
+  symbol: '$',
+  toastText: 'Copied to clipboard!',
+  toastType: 'success' as NormalTypes,
   width: 'initial',
   copy: 'default' as CopyTypes,
   type: 'default' as SnippetTypes,
@@ -38,6 +44,9 @@ const Snippet: React.FC<React.PropsWithChildren<SnippetProps>> = ({
   type,
   filled,
   children,
+  symbol,
+  toastText,
+  toastType,
   text,
   width,
   copy: copyType,
@@ -58,12 +67,16 @@ const Snippet: React.FC<React.PropsWithChildren<SnippetProps>> = ({
     if (!ref.current) return ''
     return ref.current.textContent
   }, [ref.current, children, text])
+  const symbolBefore = useMemo(() => {
+    const str = symbol.trim()
+    return str ? `${str} ` : ''
+  }, [symbol])
 
   const clickHandler = () => {
     if (!childText || !showCopyIcon) return
     copy(childText)
     if (copyType === 'slient') return
-    setToast({ text: 'Copied to clipboard!', type: 'success' })
+    setToast({ text: toastText, type: toastType })
   }
 
   return (
@@ -101,7 +114,7 @@ const Snippet: React.FC<React.PropsWithChildren<SnippetProps>> = ({
         }
 
         pre::before {
-          content: '$ ';
+          content: '${symbolBefore}';
           user-select: none;
         }
 
diff --git a/components/table/table-column.tsx b/components/table/table-column.tsx
index b79a159..45e2935 100644
--- a/components/table/table-column.tsx
+++ b/components/table/table-column.tsx
@@ -16,19 +16,19 @@ const TableColumn: React.FC<React.PropsWithChildren<TableColumnProps>> = ({
   label,
   width,
 }) => {
-  const { appendColumn } = useTableContext()
+  const { updateColumn } = useTableContext()
   if (!prop || prop.trim() === '') {
     useWarning('The props "prop" is required.', 'Table.Column')
   }
 
   useEffect(() => {
-    appendColumn &&
-      appendColumn({
+    updateColumn &&
+      updateColumn({
         label: children || label,
         value: `${prop}`.trim(),
         width,
       })
-  }, [])
+  }, [children, label, prop, width])
 
   return null
 }
diff --git a/components/table/table-context.ts b/components/table/table-context.ts
index 26ec628..a01b926 100644
--- a/components/table/table-context.ts
+++ b/components/table/table-context.ts
@@ -8,7 +8,7 @@ export type TableColumnItem = {
 
 export interface TableConfig {
   columns: Array<TableColumnItem>
-  appendColumn?: (column: TableColumnItem) => void
+  updateColumn?: (column: TableColumnItem) => void
   removeRow?: (rowIndex: number) => void
 }
 
diff --git a/components/table/table.tsx b/components/table/table.tsx
index eeea224..0637448 100644
--- a/components/table/table.tsx
+++ b/components/table/table.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useRef } from 'react'
+import React, { useEffect, useMemo, useRef, useState } from 'react'
 import TableColumn from './table-column'
 import TableHead from './table-head'
 import TableBody from './table-body'
@@ -46,11 +46,17 @@ const Table: React.FC<React.PropsWithChildren<TableProps>> = ({
 }) => {
   const ref = useRef<HTMLTableElement>(null)
   const [{ width }, updateShape] = useRealShape<HTMLTableElement>(ref)
-  const [columns, setColumns, columnsRef] = useCurrentState<Array<TableColumnItem>>([])
+  const [columns, setColumns] = useState<Array<TableColumnItem>>([])
   const [selfData, setSelfData, dataRef] = useCurrentState<Array<TableColumnItem>>([])
-  const appendColumn = (column: TableColumnItem) => {
-    const pureCurrent = columnsRef.current.filter(item => item.value !== column.value)
-    setColumns([...pureCurrent, column])
+  const updateColumn = (column: TableColumnItem) => {
+    setColumns(last => {
+      const hasColumn = last.find(item => item.value === column.value)
+      if (!hasColumn) return [...last, column]
+      return last.map(item => {
+        if (item.value !== column.value) return item
+        return column
+      })
+    })
   }
   const removeRow = (rowIndex: number) => {
     const next = dataRef.current.filter((_, index) => index !== rowIndex)
@@ -61,7 +67,7 @@ const Table: React.FC<React.PropsWithChildren<TableProps>> = ({
   const initialValue = useMemo<TableConfig>(
     () => ({
       columns,
-      appendColumn,
+      updateColumn,
       removeRow,
     }),
     [columns],
diff --git a/components/tabs/tabs-context.ts b/components/tabs/tabs-context.ts
index f9150c2..19a9fd8 100644
--- a/components/tabs/tabs-context.ts
+++ b/components/tabs/tabs-context.ts
@@ -8,7 +8,6 @@ export interface TabsLabelItem {
 
 export interface TabsConfig {
   register?: (item: TabsLabelItem) => void
-  unregister?: (item: TabsLabelItem) => void
   currentValue?: string
   inGroup: boolean
 }
diff --git a/components/tabs/tabs-item.tsx b/components/tabs/tabs-item.tsx
index 30f18d3..e128b5d 100644
--- a/components/tabs/tabs-item.tsx
+++ b/components/tabs/tabs-item.tsx
@@ -4,7 +4,7 @@ import { useTabsContext } from './tabs-context'
 
 interface Props {
   label: string | React.ReactNode
-  value?: string
+  value: string
   disabled?: boolean
 }
 
@@ -16,20 +16,16 @@ export type TabsItemProps = Props & typeof defaultProps
 
 const TabsItem: React.FC<React.PropsWithChildren<TabsItemProps>> = ({
   children,
-  value: userCustomValue,
+  value,
   label,
   disabled,
 }) => {
-  const value = useMemo(() => userCustomValue || `${label}`, [userCustomValue, label])
-  const { register, unregister, currentValue } = useTabsContext()
+  const { register, currentValue } = useTabsContext()
   const isActive = useMemo(() => currentValue === value, [currentValue, value])
 
   useEffect(() => {
     register && register({ value, label, disabled })
-    return () => {
-      unregister && unregister({ value, label, disabled })
-    }
-  }, [])
+  }, [value, label, disabled])
 
   /* eslint-disable react/jsx-no-useless-fragment */
   return isActive ? <>{children}</> : null
diff --git a/components/tabs/tabs.tsx b/components/tabs/tabs.tsx
index f00f053..89d900f 100644
--- a/components/tabs/tabs.tsx
+++ b/components/tabs/tabs.tsx
@@ -2,8 +2,6 @@ import React, { useEffect, useMemo, useState } from 'react'
 import TabsItem from './tabs-item'
 import useTheme from '../styles/use-theme'
 import { TabsLabelItem, TabsConfig, TabsContext } from './tabs-context'
-import useCurrentState from '../utils/use-current-state'
-import useWarning from '../utils/use-warning'
 
 interface Props {
   initialValue?: string
@@ -32,24 +30,26 @@ const Tabs: React.FC<React.PropsWithChildren<TabsProps>> = ({
 }) => {
   const theme = useTheme()
   const [selfValue, setSelfValue] = useState<string | undefined>(userCustomInitialValue)
-  const [tabs, setTabs, tabsRef] = useCurrentState<Array<TabsLabelItem>>([])
+  const [tabs, setTabs] = useState<Array<TabsLabelItem>>([])
 
   const register = (next: TabsLabelItem) => {
-    const hasItem = tabsRef.current.find(item => item.value === next.value)
-    if (hasItem) {
-      useWarning('The "value" of each "Tabs.Item" must be unique.', 'Tabs')
-    }
-    setTabs([...tabsRef.current, next])
-  }
-  const unregister = (next: TabsLabelItem) => {
-    const nextTabs = tabsRef.current.filter(item => item.value !== next.value)
-    setTabs([...nextTabs])
+    setTabs(last => {
+      const hasItem = last.find(item => item.value === next.value)
+      if (!hasItem) return [...last, next]
+      return last.map(item => {
+        if (item.value !== next.value) return item
+        return {
+          ...item,
+          label: next.label,
+          disabled: next.disabled,
+        }
+      })
+    })
   }
 
   const initialValue = useMemo<TabsConfig>(
     () => ({
       register,
-      unregister,
       currentValue: selfValue,
       inGroup: true,
     }),
@@ -71,13 +71,13 @@ const Tabs: React.FC<React.PropsWithChildren<TabsProps>> = ({
     <TabsContext.Provider value={initialValue}>
       <div className={`tabs ${className}`} {...props}>
         <header className={hideDivider ? 'hide-divider' : ''}>
-          {tabs.map((item, index) => (
+          {tabs.map(item => (
             <div
               className={`tab ${selfValue === item.value ? 'active' : ''} ${
                 item.disabled ? 'disabled' : ''
               }`}
               role="button"
-              key={item.value + index}
+              key={item.value}
               onClick={() => clickHandler(item)}>
               {item.label}
             </div>
diff --git a/components/textarea/textarea.tsx b/components/textarea/textarea.tsx
index 1cffa1c..f563830 100644
--- a/components/textarea/textarea.tsx
+++ b/components/textarea/textarea.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useState } from 'react'
+import React, { useRef, useImperativeHandle, useEffect, useMemo, useState } from 'react'
 import useTheme from '../styles/use-theme'
 import withDefaults from '../utils/with-defaults'
 import { NormalTypes } from '../utils/prop-types'
@@ -32,113 +32,130 @@ const defaultProps = {
 type NativeAttrs = Omit<React.TextareaHTMLAttributes<any>, keyof Props>
 export type TextareaProps = Props & typeof defaultProps & NativeAttrs
 
-const Textarea: React.FC<React.PropsWithChildren<TextareaProps>> = ({
-  width,
-  status,
-  minHeight,
-  disabled,
-  readOnly,
-  onFocus,
-  onBlur,
-  className,
-  initialValue,
-  onChange,
-  value,
-  placeholder,
-  ...props
-}) => {
-  const theme = useTheme()
-  const [selfValue, setSelfValue] = useState<string>(initialValue)
-  const [hover, setHover] = useState<boolean>(false)
-  const { color, borderColor, hoverBorder } = useMemo(() => getColors(theme.palette, status), [
-    theme.palette,
-    status,
-  ])
+const Textarea = React.forwardRef<HTMLTextAreaElement, React.PropsWithChildren<TextareaProps>>(
+  (
+    {
+      width,
+      status,
+      minHeight,
+      disabled,
+      readOnly,
+      onFocus,
+      onBlur,
+      className,
+      initialValue,
+      onChange,
+      value,
+      placeholder,
+      ...props
+    },
+    ref: React.Ref<HTMLTextAreaElement | null>,
+  ) => {
+    const theme = useTheme()
+    const textareaRef = useRef<HTMLTextAreaElement>(null)
+    useImperativeHandle(ref, () => textareaRef.current)
+    const isControlledComponent = useMemo(() => value !== undefined, [value])
+    const [selfValue, setSelfValue] = useState<string>(initialValue)
+    const [hover, setHover] = useState<boolean>(false)
+    const { color, borderColor, hoverBorder } = useMemo(() => getColors(theme.palette, status), [
+      theme.palette,
+      status,
+    ])
 
-  const changeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
-    if (disabled || readOnly) return
-    setSelfValue(event.target.value)
-    onChange && onChange(event)
-  }
-  const focusHandler = (e: React.FocusEvent<HTMLTextAreaElement>) => {
-    setHover(true)
-    onFocus && onFocus(e)
-  }
-  const blurHandler = (e: React.FocusEvent<HTMLTextAreaElement>) => {
-    setHover(false)
-    onBlur && onBlur(e)
-  }
+    const changeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
+      if (disabled || readOnly) return
+      setSelfValue(event.target.value)
+      onChange && onChange(event)
+    }
+    const focusHandler = (e: React.FocusEvent<HTMLTextAreaElement>) => {
+      setHover(true)
+      onFocus && onFocus(e)
+    }
+    const blurHandler = (e: React.FocusEvent<HTMLTextAreaElement>) => {
+      setHover(false)
+      onBlur && onBlur(e)
+    }
 
-  useEffect(() => {
-    if (value === undefined) return
-    setSelfValue(value)
-  }, [value])
+    useEffect(() => {
+      if (isControlledComponent) {
+        setSelfValue(value as string)
+      }
+    })
 
-  return (
-    <div className={`wrapper ${hover ? 'hover' : ''} ${disabled ? 'disabled' : ''} ${className}`}>
-      <textarea
-        disabled={disabled}
-        value={selfValue}
-        placeholder={placeholder}
-        readOnly={readOnly}
-        onFocus={focusHandler}
-        onBlur={blurHandler}
-        onChange={changeHandler}
-        {...props}
-      />
-      <style jsx>{`
-        .wrapper {
-          display: inline-flex;
-          box-sizing: border-box;
-          user-select: none;
-          width: ${width};
-          min-width: 12.5rem;
-          max-width: 95vw;
-          height: auto;
-          border-radius: ${theme.layout.radius};
-          border: 1px solid ${borderColor};
-          color: ${color};
-          transition: border 0.2s ease 0s, color 0.2s ease 0s;
-        }
+    const controlledValue = isControlledComponent
+      ? { value: selfValue }
+      : { defaultValue: initialValue }
+    const textareaProps = {
+      ...props,
+      ...controlledValue,
+    }
 
-        .wrapper.hover {
-          border-color: ${hoverBorder};
-        }
+    return (
+      <div className={`wrapper ${hover ? 'hover' : ''} ${disabled ? 'disabled' : ''} ${className}`}>
+        <textarea
+          ref={textareaRef}
+          disabled={disabled}
+          placeholder={placeholder}
+          readOnly={readOnly}
+          onFocus={focusHandler}
+          onBlur={blurHandler}
+          onChange={changeHandler}
+          {...textareaProps}
+        />
+        <style jsx>{`
+          .wrapper {
+            display: inline-flex;
+            box-sizing: border-box;
+            user-select: none;
+            width: ${width};
+            min-width: 12.5rem;
+            max-width: 95vw;
+            height: auto;
+            border-radius: ${theme.layout.radius};
+            border: 1px solid ${borderColor};
+            color: ${color};
+            transition: border 0.2s ease 0s, color 0.2s ease 0s;
+          }
 
-        .wrapper.disabled {
-          background-color: ${theme.palette.accents_1};
-          border-color: ${theme.palette.accents_2};
-          cursor: not-allowed;
-        }
+          .wrapper.hover {
+            border-color: ${hoverBorder};
+          }
 
-        textarea {
-          background-color: transparent;
-          box-shadow: none;
-          display: block;
-          font-family: ${theme.font.sans};
-          font-size: 0.875rem;
-          width: 100%;
-          height: 100%;
-          min-height: ${minHeight};
-          resize: none;
-          border: none;
-          outline: none;
-          padding: ${theme.layout.gapHalf};
-        }
+          .wrapper.disabled {
+            background-color: ${theme.palette.accents_1};
+            border-color: ${theme.palette.accents_2};
+            cursor: not-allowed;
+          }
 
-        .disabled > textarea {
-          cursor: not-allowed;
-        }
+          textarea {
+            background-color: transparent;
+            box-shadow: none;
+            display: block;
+            font-family: ${theme.font.sans};
+            font-size: 0.875rem;
+            width: 100%;
+            height: 100%;
+            min-height: ${minHeight};
+            resize: none;
+            border: none;
+            outline: none;
+            padding: ${theme.layout.gapHalf};
+          }
 
-        textarea:-webkit-autofill,
-        textarea:-webkit-autofill:hover,
-        textarea:-webkit-autofill:active,
-        textarea:-webkit-autofill:focus {
-          -webkit-box-shadow: 0 0 0 30px ${theme.palette.background} inset !important;
-        }
-      `}</style>
-    </div>
-  )
-}
+          .disabled > textarea {
+            cursor: not-allowed;
+          }
+
+          textarea:-webkit-autofill,
+          textarea:-webkit-autofill:hover,
+          textarea:-webkit-autofill:active,
+          textarea:-webkit-autofill:focus {
+            -webkit-box-shadow: 0 0 0 30px ${theme.palette.background} inset !important;
+          }
+        `}</style>
+      </div>
+    )
+  },
+)
 
 export default withDefaults(Textarea, defaultProps)
diff --git a/components/utils/collections.ts b/components/utils/collections.ts
index a6cb23e..149d502 100644
--- a/components/utils/collections.ts
+++ b/components/utils/collections.ts
@@ -111,3 +111,16 @@ export const getReactNode = (node?: React.ReactNode | (() => React.ReactNode)):
   if (typeof node !== 'function') return node
   return (node as () => React.ReactNode)()
 }
+
+export const isChildElement = (
+  parent: Element | null | undefined,
+  child: Element | null | undefined,
+): boolean => {
+  if (!parent || !child) return false
+  let node: (Node & ParentNode) | null = child
+  while (node) {
+    if (node === parent) return true
+    node = node.parentNode
+  }
+  return false
+}
diff --git a/components/utils/use-portal.ts b/components/utils/use-portal.ts
index 8ea76dc..4361bfe 100644
--- a/components/utils/use-portal.ts
+++ b/components/utils/use-portal.ts
@@ -8,7 +8,10 @@ const createElement = (id: string): HTMLElement => {
   return el
 }
 
-const usePortal = (selectId: string = getId()): HTMLElement | null => {
+const usePortal = (
+  selectId: string = getId(),
+  getContainer?: () => HTMLElement | null,
+): HTMLElement | null => {
   const id = `zeit-ui-${selectId}`
   const { isBrowser } = useSSR()
   const [elSnapshot, setElSnapshot] = useState<HTMLElement | null>(
@@ -16,11 +19,13 @@ const usePortal = (selectId: string = getId()): HTMLElement | null => {
   )
 
   useEffect(() => {
-    const hasElement = document.querySelector<HTMLElement>(`#${id}`)
+    const customContainer = getContainer ? getContainer() : null
+    const parentElement = customContainer || document.body
+    const hasElement = parentElement.querySelector<HTMLElement>(`#${id}`)
     const el = hasElement || createElement(id)
 
     if (!hasElement) {
-      document.body.appendChild(el)
+      parentElement.appendChild(el)
     }
     setElSnapshot(el)
   }, [])
diff --git a/lib/components/menu/menu-links.tsx b/lib/components/menu/menu-links.tsx
index 82a9476..78b9378 100644
--- a/lib/components/menu/menu-links.tsx
+++ b/lib/components/menu/menu-links.tsx
@@ -3,7 +3,7 @@ import Router from 'next/router'
 import { useTheme } from 'components'
 import Controls from 'lib/components/controls'
 import LogoIcon from 'lib/components/icons/logo'
-import { useConfigs } from '../../config-context'
+import { useConfigs } from 'lib/config-context'
 
 const MenuLinks = () => {
   const theme = useTheme()
diff --git a/package.json b/package.json
index 0035eaa..0702aa1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@zeit-ui/react",
-  "version": "1.7.7",
+  "version": "1.8.0-canary.10",
   "main": "dist/index.js",
   "module": "esm/index.js",
   "types": "dist/index.d.ts",
diff --git a/pages/en-us/components/button.mdx b/pages/en-us/components/button.mdx
index 53c3e08..111d352 100644
--- a/pages/en-us/components/button.mdx
+++ b/pages/en-us/components/button.mdx
@@ -110,7 +110,7 @@ Used to trigger an operation.
   scope={{ Button, Spacer, Settings, UserX, Power }}
   code={`
 <>
-  <Button iconRight={<Power />} auto size="small">shut off</Button>
+  <Button iconRight={<Power />} auto size="small" />
   <Spacer y={.5} />
   <Button icon={<Settings />} auto>Action</Button>
   <Spacer y={.5} />
diff --git a/pages/en-us/components/input.mdx b/pages/en-us/components/input.mdx
index 7e96681..a906195 100644
--- a/pages/en-us/components/input.mdx
+++ b/pages/en-us/components/input.mdx
@@ -185,6 +185,28 @@ Retrieve text input from a user.
 `}
 />
 
+<Playground
+  title="Imperative API"
+  desc="Update component in an uncontrolled way."
+  scope={{ Input, Spacer, Button }}
+  code={`
+() => {
+  const ref = React.useRef(null)
+  const setChange = () => {
+    ref && (ref.current.value = Math.random().toString(32))
+  }
+  return (
+    <>
+      <Input initialValue="Hello" onChange={e => console.log(e.target.value)} ref={ref} />
+      <Spacer y={.5} />
+      <Button auto type="secondary" size="small"
+        onClick={setChange}>set value</Button>
+    </>
+  )
+}
+`}
+/>
+
 <Attributes edit="/pages/en-us/components/input.mdx">
 <Attributes.Title>Input.Props</Attributes.Title>
 
diff --git a/pages/en-us/components/link.mdx b/pages/en-us/components/link.mdx
index b41ef65..4c66a87 100644
--- a/pages/en-us/components/link.mdx
+++ b/pages/en-us/components/link.mdx
@@ -49,7 +49,7 @@ Hyperlinks between pages.
   <Text><Link href="#">The Evil Rabbit Jumped over the Fence.</Link></Text>
   <Text><Link href="#" color>The Evil Rabbit Jumped over the Fence.</Link></Text>
   <Text><Link href="#" underline>The Evil Rabbit Jumped over the Fence.</Link></Text>
-  <Text><Link href="#" underline>The Evil Rabbit Jumped over the Fence.</Link></Text>
+  <Text><Link href="#" color underline>The Evil Rabbit Jumped over the Fence.</Link></Text>
 </>
 `}
 />
diff --git a/pages/en-us/components/modal.mdx b/pages/en-us/components/modal.mdx
index 0084f53..808e43f 100644
--- a/pages/en-us/components/modal.mdx
+++ b/pages/en-us/components/modal.mdx
@@ -159,6 +159,30 @@ Display popup content that requires attention or provides additional information
 `}
 />
 
+<Playground
+  title="Loading"
+  scope={{ Modal, Button, useModal }}
+  code={`
+() => {
+  const { visible, setVisible, bindings } = useModal()
+  return (
+    <>
+      <Button auto onClick={() => setVisible(true)}>Show Modal</Button>
+      <Modal {...bindings}>
+        <Modal.Title>Modal</Modal.Title>
+        <Modal.Subtitle>This is a modal</Modal.Subtitle>
+        <Modal.Content>
+          <p>Some content contained within the modal.</p>
+        </Modal.Content>
+        <Modal.Action passive onClick={() => setVisible(false)}>Cancel</Modal.Action>
+        <Modal.Action loading>Submit</Modal.Action>
+      </Modal>
+    </>
+  )
+}
+`}
+/>
+
 <Attributes edit="/pages/en-us/components/modal.mdx">
 <Attributes.Title>Modal.Props</Attributes.Title>
 
@@ -190,12 +214,6 @@ Display popup content that requires attention or provides additional information
 | --------- | ------------ | ---------------- | ------------------------ | ------- |
 | ...       | native props | `HTMLAttributes` | `'id', 'className', ...` | -       |
 
-<Attributes.Title>Modal.Actions.Props</Attributes.Title>
-
-| Attribute | Description | Type | Accepted values | Default |
-| --------- | ----------- | ---- | --------------- | ------- |
-| -         | -           | -    | -               | -       |
-
 <Attributes.Title>Modal.Action.Props</Attributes.Title>
 
 | Attribute    | Description            | Type                                                   | Accepted values          | Default |
@@ -203,6 +221,7 @@ Display popup content that requires attention or provides additional information
 | **passive**  | display passive mode   | `boolean`                                              | -                        | `false` |
 | **disabled** | disable current action | `boolean`                                              | -                        | `false` |
 | **onClick**  | click handler          | [(event: ModalActionEvent) => void](#modalactionevent) | -                        | -       |
+| **loading**  | show loading           | `boolean`                                              | -                        | `false` |
 | ...          | native props           | `ButtonHTMLAttributes`                                 | `'id', 'className', ...` | -       |
 
 <Attributes.Title>useModal</Attributes.Title>
diff --git a/pages/en-us/components/select.mdx b/pages/en-us/components/select.mdx
index cefe6cd..029b475 100644
--- a/pages/en-us/components/select.mdx
+++ b/pages/en-us/components/select.mdx
@@ -1,5 +1,5 @@
 import { Layout, Playground, Attributes } from 'lib/components'
-import { Select, Spacer, Code } from 'components'
+import { Select, Spacer, Code, Modal, useModal, Button } from 'components'
 
 export const meta = {
   title: 'select',
@@ -142,25 +142,56 @@ Display a dropdown list of items.
 `}
 />
 
+<Playground
+  title="Set parent element"
+  desc="you can specify the container for drop-down box rendering."
+  scope={{ Select, Spacer, useModal, Modal, Button, Code }}
+  code={`
+() => {
+  const { visible, setVisible, bindings } = useModal()
+    return (
+      <>
+        <Button auto onClick={() => setVisible(true)}>Show Select</Button>
+        <Modal {...bindings}>
+          <Modal.Title>Modal</Modal.Title>
+          <Modal.Content id="customModalSelect">
+            <Select placeholder="Choose one" initialValue="1"
+              getPopupContainer={() => document.getElementById('customModalSelect')}>
+              <Select.Option value="1"><Code>TypeScript</Code></Select.Option>
+              <Select.Option value="2"><Code>JavaScript</Code></Select.Option>
+            </Select>
+            <p>Scroll through the content to see the changes.</p>
+            <div style={{ height: '1200px' }}></div>
+            <p>Scroll through the content to see the changes.</p>
+          </Modal.Content>
+          <Modal.Action passive onClick={() => setVisible(false)}>Cancel</Modal.Action>
+        </Modal>
+      </>
+    )
+}
+`}
+/>
+
 <Attributes edit="/pages/en-us/components/select.mdx">
 <Attributes.Title>Select.Props</Attributes.Title>
 
-| Attribute             | Description                             | Type                                                | Accepted values                   | Default         |
-| --------------------- | --------------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
-| **value**             | selected value                          | `string`, `string[]`                                | -                                 | -               |
-| **initialValue**      | initial value                           | `string`, `string[]`                                | -                                 | -               |
-| **placeholder**       | placeholder string                      | `string`                                            | -                                 | -               |
-| **width**             | css width value of select               | `string`                                            | -                                 | `initial`       |
-| **size**              | select component size                   | `NormalSizes`                                       | [NormalSizes](#normalsizes)       | `medium`        |
-| **icon**              | icon component                          | `ComponentType`                                     | -                                 | `SVG Component` |
-| **pure**              | remove icon component                   | `boolean`                                           | -                                 | `false`         |
-| **multiple**          | support multiple selection              | `boolean`                                           | -                                 | `false`         |
-| **disabled**          | disable current radio                   | `boolean`                                           | -                                 | `false`         |
-| **onChange**          | selected value                          | <Code>(val: string &#124; string[]) => void </Code> | -                                 | -               |
-| **dropdownClassName** | className of dropdown menu              | `string`                                            | -                                 | -               |
-| **dropdownStyle**     | style of dropdown menu                  | `object`                                            | -                                 | -               |
-| **disableMatchWidth** | disable Option from follow Select width | `boolean`                                           | -                                 | `false`         |
-| ...                   | native props                            | `HTMLAttributes`                                    | `'name', 'alt', 'className', ...` | -               |
+| Attribute             | Description                                           | Type                                                | Accepted values                   | Default         |
+| --------------------- | ----------------------------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
+| **value**             | selected value                                        | `string`, `string[]`                                | -                                 | -               |
+| **initialValue**      | initial value                                         | `string`, `string[]`                                | -                                 | -               |
+| **placeholder**       | placeholder string                                    | `string`                                            | -                                 | -               |
+| **width**             | css width value of select                             | `string`                                            | -                                 | `initial`       |
+| **size**              | select component size                                 | `NormalSizes`                                       | [NormalSizes](#normalsizes)       | `medium`        |
+| **icon**              | icon component                                        | `ComponentType`                                     | -                                 | `SVG Component` |
+| **pure**              | remove icon component                                 | `boolean`                                           | -                                 | `false`         |
+| **multiple**          | support multiple selection                            | `boolean`                                           | -                                 | `false`         |
+| **disabled**          | disable current radio                                 | `boolean`                                           | -                                 | `false`         |
+| **onChange**          | selected value                                        | <Code>(val: string &#124; string[]) => void </Code> | -                                 | -               |
+| **dropdownClassName** | className of dropdown menu                            | `string`                                            | -                                 | -               |
+| **dropdownStyle**     | style of dropdown menu                                | `object`                                            | -                                 | -               |
+| **disableMatchWidth** | disable Option from follow Select width               | `boolean`                                           | -                                 | `false`         |
+| **getPopupContainer** | dropdown render parent element, the default is `body` | `() => HTMLElement`                                 | -                                 | -               |
+| ...                   | native props                                          | `HTMLAttributes`                                    | `'name', 'alt', 'className', ...` | -               |
 
 <Attributes.Title>Select.Option.Props</Attributes.Title>
 
diff --git a/pages/en-us/components/snippet.mdx b/pages/en-us/components/snippet.mdx
index d25cdef..69df6cb 100644
--- a/pages/en-us/components/snippet.mdx
+++ b/pages/en-us/components/snippet.mdx
@@ -65,6 +65,22 @@ Display a snippet of copyable code for the command line.
 `}
 />
 
+<Playground
+  title="custom symbol"
+  scope={{ Snippet }}
+  code={`
+<Snippet symbol=">" text="yarn add @zeit-ui/react" width="300px" />
+`}
+/>
+
+<Playground
+  title="custom toast"
+  scope={{ Snippet }}
+  code={`
+<Snippet toastText="Code copied!" toastType="secondary" text="yarn add @zeit-ui/react" width="300px" />
+`}
+/>
+
 <Playground
   title="filled"
   scope={{ Snippet, Spacer }}
@@ -84,14 +100,17 @@ Display a snippet of copyable code for the command line.
 <Attributes edit="/pages/en-us/components/snippet.mdx">
 <Attributes.Title>Snippet.Props</Attributes.Title>
 
-| Attribute  | Description             | Type                          | Accepted values                  | Default   |
-| ---------- | ----------------------- | ----------------------------- | -------------------------------- | --------- |
-| **text**   | code snippet            | `string` `Array<string>`      | -                                | -         |
-| **type**   | snippet types           | [SnippetTypes](#snippettypes) | [SnippetTypes](#snippettypes)    | `default` |
-| **filled** | filled style            | `boolean`                     | -                                | `false`   |
-| **width**  | set CSS string          | `string`                      | -                                | `initial` |
-| **copy**   | function of copy button | `CopyTypes`                   | `'default', 'slient', 'prevent'` | `default` |
-| ...        | native props            | `HTMLAttributes`              | `'id', 'name', 'className', ...` | -         |
+| Attribute     | Description             | Type                          | Accepted values                  | Default                |
+| ------------- | ----------------------- | ----------------------------- | -------------------------------- | ---------------------- |
+| **text**      | code snippet            | `string` `Array<string>`      | -                                | -                      |
+| **type**      | snippet types           | [SnippetTypes](#snippettypes) | [SnippetTypes](#snippettypes)    | `default`              |
+| **filled**    | filled style            | `boolean`                     | -                                | `false`                |
+| **width**     | set CSS string          | `string`                      | -                                | `initial`              |
+| **copy**      | function of copy button | `CopyTypes`                   | `'default', 'slient', 'prevent'` | `default`              |
+| **symbol**    | symbol snippet          | `string`                      | -                                | `$`                    |
+| **toastText** | toast text              | `string`                      | -                                | `Copied to clipboard!` |
+| **toastType** | toast type              | `NormalTypes`                 | [NormalTypes](#normaltypes)      | `success`              |
+| ...           | native props            | `HTMLAttributes`              | `'id', 'name', 'className', ...` | -                      |
 
 <Attributes.Title>SnippetTypes</Attributes.Title>
 
@@ -99,6 +118,12 @@ Display a snippet of copyable code for the command line.
 type SnippetTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error' | 'dark' | 'lite'
 ```
 
+<Attributes.Title>NormalTypes</Attributes.Title>
+
+```ts
+type NormalTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error'
+```
+
 </Attributes>
 
 export default ({ children }) => <Layout meta={meta}>{children}</Layout>
diff --git a/pages/en-us/components/tabs.mdx b/pages/en-us/components/tabs.mdx
index 60f1cb1..c1fef3f 100644
--- a/pages/en-us/components/tabs.mdx
+++ b/pages/en-us/components/tabs.mdx
@@ -134,7 +134,7 @@ Display tab content.
 | Attribute           | Description         | Type      | Accepted values | Default |
 | ------------------- | ------------------- | --------- | --------------- | ------- |
 | **label**(required) | display tab's label | `string`  | -               | -       |
-| **value**           | unique ident value  | `string`  | -               | -       |
+| **value**(required) | unique ident value  | `string`  | -               | -       |
 | **disabled**        | disable current tab | `boolean` | -               | `false` |
 
 <Attributes.Title>useTabs</Attributes.Title>
diff --git a/pages/en-us/components/textarea.mdx b/pages/en-us/components/textarea.mdx
index f9363bb..981f4f7 100644
--- a/pages/en-us/components/textarea.mdx
+++ b/pages/en-us/components/textarea.mdx
@@ -99,6 +99,28 @@ Retrieve multi-line user input.
 `}
 />
 
+<Playground
+  title="Imperative API"
+  desc="Update component in an uncontrolled way."
+  scope={{ Textarea, Spacer, Button }}
+  code={`
+() => {
+  const ref = React.useRef(null)
+  const setChange = () => {
+    ref && (ref.current.value = Math.random().toString(32))
+  }
+  return (
+    <>
+      <Textarea initialValue="Hello" onChange={e => console.log(e.target.value)} ref={ref} />
+      <Spacer y={.5} />
+      <Button auto type="secondary" size="small"
+        onClick={setChange}>set value</Button>
+    </>
+  )
+}
+`}
+/>
+
 <Attributes edit="/pages/en-us/components/textarea.mdx">
 <Attributes.Title alias="Input.Textarea">Textarea.Props</Attributes.Title>
 
diff --git a/pages/en-us/components/toast.mdx b/pages/en-us/components/toast.mdx
index 511e317..8af02bc 100644
--- a/pages/en-us/components/toast.mdx
+++ b/pages/en-us/components/toast.mdx
@@ -125,6 +125,16 @@ interface ToastAction {
 }
 ```
 
+<Attributes.Title>NormalTypes</Attributes.Title>
+
+```ts
+type NormalTypes = 'default'
+  | 'secondary'
+  | 'success'
+  | 'warning'
+  | 'error'
+```
+
 </Attributes>
 
 export default ({ children }) => <Layout meta={meta}>{children}</Layout>
diff --git a/pages/zh-cn/components/button.mdx b/pages/zh-cn/components/button.mdx
index 8b62e05..6e6b1ae 100644
--- a/pages/zh-cn/components/button.mdx
+++ b/pages/zh-cn/components/button.mdx
@@ -109,7 +109,7 @@ export const meta = {
   scope={{ Button, Spacer, Settings, UserX, Power }}
   code={`
 <>
-  <Button iconRight={<Power />} auto size="small">关闭</Button>
+  <Button iconRight={<Power />} auto size="small" />
   <Spacer y={.5} />
   <Button icon={<Settings />} auto>按钮</Button>
   <Spacer y={.5} />
diff --git a/pages/zh-cn/components/input.mdx b/pages/zh-cn/components/input.mdx
index 07d6269..ae7c284 100644
--- a/pages/zh-cn/components/input.mdx
+++ b/pages/zh-cn/components/input.mdx
@@ -186,6 +186,28 @@ export const meta = {
 `}
 />
 
+<Playground
+  title="命令式 API"
+  desc="使用非受控方式更新组件。"
+  scope={{ Input, Spacer, Button }}
+  code={`
+() => {
+  const ref = React.useRef(null)
+  const setChange = () => {
+    ref && (ref.current.value = Math.random().toString(32))
+  }
+  return (
+    <>
+      <Input initialValue="Hello" onChange={e => console.log(e.target.value)} ref={ref} />
+      <Spacer y={.5} />
+      <Button auto type="secondary" size="small"
+        onClick={setChange}>设置值</Button>
+    </>
+  )
+}
+`}
+/>
+
 <Attributes edit="/pages/zh-cn/components/input.mdx">
 <Attributes.Title>Input.Props</Attributes.Title>
 
diff --git a/pages/zh-cn/components/link.mdx b/pages/zh-cn/components/link.mdx
index 2d84ee9..6d07420 100644
--- a/pages/zh-cn/components/link.mdx
+++ b/pages/zh-cn/components/link.mdx
@@ -37,7 +37,7 @@ export const meta = {
   <Text><Link href="#">严格模式通过抛出错误来消除了一些原有静默错误</Link></Text>
   <Text><Link href="#" color>严格模式通过抛出错误来消除了一些原有静默错误</Link></Text>
   <Text><Link href="#" underline>通过使用指南和教程来学习如何用 React</Link></Text>
-  <Text><Link href="#" underline>通过使用指南和教程来学习如何用 React</Link></Text>
+  <Text><Link href="#" color underline>通过使用指南和教程来学习如何用 React</Link></Text>
 </>
 `}
 />
diff --git a/pages/zh-cn/components/modal.mdx b/pages/zh-cn/components/modal.mdx
index 1c16072..f4ade63 100644
--- a/pages/zh-cn/components/modal.mdx
+++ b/pages/zh-cn/components/modal.mdx
@@ -40,8 +40,8 @@ export const meta = {
         <Modal.Content>
           <p> <Code>yield</Code> 关键字通常不能在箭头函数中使用（除非是嵌套在允许使用的函数内）。因此，箭头函数不能用作函数生成器。</p>
         </Modal.Content>
-        <Modal.Action passive>放弃使用</Modal.Action>
-        <Modal.Action>明白了</Modal.Action>
+        <Modal.Action passive onClick={({ close }) => close()}>放弃使用</Modal.Action>
+        <Modal.Action onClick={({ close }) => close()}>明白了</Modal.Action>
       </Modal>
     </div>
   )
@@ -69,8 +69,8 @@ export const meta = {
         <Modal.Content>
           <p> <Code>yield</Code> 关键字通常不能在箭头函数中使用（除非是嵌套在允许使用的函数内）。因此，箭头函数不能用作函数生成器。</p>
         </Modal.Content>
-        <Modal.Action passive>放弃使用</Modal.Action>
-        <Modal.Action>明白了</Modal.Action>
+        <Modal.Action passive onClick={() => setVisible(false)}>放弃使用</Modal.Action>
+        <Modal.Action onClick={() => setVisible(false)}>明白了</Modal.Action>
       </Modal>
     </>
   )
@@ -127,8 +127,8 @@ export const meta = {
         <Modal.Content>
           <p> <Code>yield</Code> 关键字通常不能在箭头函数中使用（除非是嵌套在允许使用的函数内）。因此，箭头函数不能用作函数生成器。</p>
         </Modal.Content>
-        <Modal.Action passive>继续学习</Modal.Action>
-        <Modal.Action disabled>尝试箭头函数</Modal.Action>
+        <Modal.Action passive onClick={({ close }) => close()}>继续学习</Modal.Action>
+        <Modal.Action disabled onClick={close => close()}>尝试箭头函数</Modal.Action>
       </Modal>
     </>
   )
@@ -158,6 +158,30 @@ export const meta = {
 `}
 />
 
+<Playground
+  title="加载中"
+  scope={{ Modal, Button, useModal, Code }}
+  code={`
+() => {
+  const { visible, setVisible, bindings } = useModal()
+  return (
+    <>
+      <Button auto onClick={() => setVisible(true)}>Show Modal</Button>
+      <Modal {...bindings}>
+        <Modal.Title>箭头函数</Modal.Title>
+        <Modal.Subtitle>它不能用作构造函数</Modal.Subtitle>
+        <Modal.Content>
+          <p> <Code>yield</Code> 关键字通常不能在箭头函数中使用（除非是嵌套在允许使用的函数内）。因此，箭头函数不能用作函数生成器。</p>
+        </Modal.Content>
+        <Modal.Action passive onClick={() => setVisible(false)}>取消</Modal.Action>
+        <Modal.Action loading>提交</Modal.Action>
+      </Modal>
+    </>
+  )
+}
+`}
+/>
+
 <Attributes edit="/pages/zh-cn/components/modal.mdx">
 <Attributes.Title>Modal.Props</Attributes.Title>
 
@@ -189,12 +213,6 @@ export const meta = {
 | ---- | -------- | ---------------- | ------------------------ | ---- |
 | ...  | 原生属性 | `HTMLAttributes` | `'id', 'className', ...` | -    |
 
-<Attributes.Title>Modal.Actions.Props</Attributes.Title>
-
-| 属性 | 描述 | 类型 | 推荐值 | 默认 |
-| ---- | ---- | ---- | ------ | ---- |
-| -    | -    | -    | -      | -    |
-
 <Attributes.Title>Modal.Action.Props</Attributes.Title>
 
 | 属性         | 描述                 | 类型                                                   | 推荐值                   | 默认    |
@@ -202,6 +220,7 @@ export const meta = {
 | **passive**  | 以消极的状态显示按钮 | `boolean`                                              | -                        | `false` |
 | **disabled** | 禁用按钮             | `boolean`                                              | -                        | `false` |
 | **onClick**  | 按钮的点击事件       | [(event: ModalActionEvent) => void](#modalactionevent) | -                        | -       |
+| **loading**  | 显示加载状态指示器   | `boolean`                                              | -                        | `false` |
 | ...          | 原生属性             | `ButtonHTMLAttributes`                                 | `'id', 'className', ...` | -       |
 
 <Attributes.Title>useModal</Attributes.Title>
diff --git a/pages/zh-cn/components/select.mdx b/pages/zh-cn/components/select.mdx
index a385d85..ee24879 100644
--- a/pages/zh-cn/components/select.mdx
+++ b/pages/zh-cn/components/select.mdx
@@ -1,5 +1,5 @@
 import { Layout, Playground, Attributes } from 'lib/components'
-import { Select, Spacer, Code } from 'components'
+import { Select, Spacer, Code, Modal, useModal, Button } from 'components'
 
 export const meta = {
   title: '选择器 Select',
@@ -142,25 +142,56 @@ export const meta = {
 `}
 />
 
+<Playground
+  title="设置渲染容器"
+  desc="你可以指定下拉框的元素渲染的容器。"
+  scope={{ Select, Spacer, useModal, Modal, Button, Code }}
+  code={`
+() => {
+  const { visible, setVisible, bindings } = useModal()
+    return (
+      <>
+        <Button auto onClick={() => setVisible(true)}>显示选择器</Button>
+        <Modal {...bindings}>
+          <Modal.Title>Modal</Modal.Title>
+          <Modal.Content id="customModalSelect">
+            <Select placeholder="选择语言" initialValue="1"
+              getPopupContainer={() => document.getElementById('customModalSelect')}>
+              <Select.Option value="1"><Code>TypeScript</Code></Select.Option>
+              <Select.Option value="2"><Code>JavaScript</Code></Select.Option>
+            </Select>
+            <p>滚动以查看下拉框的工作方式。(超出弹出层会被隐藏)</p>
+            <div style={{ height: '1200px' }}></div>
+            <p>滚动以查看下拉框的工作方式。</p>
+          </Modal.Content>
+          <Modal.Action passive onClick={() => setVisible(false)}>取消</Modal.Action>
+        </Modal>
+      </>
+    )
+}
+`}
+/>
+
 <Attributes edit="/pages/zh-cn/components/select.mdx">
 <Attributes.Title>Select.Props</Attributes.Title>
 
-| 属性                  | 描述                         | 类型                                                | 推荐值                            | 默认            |
-| --------------------- | ---------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
-| **value**             | 手动设置选择器的值           | `string`, `string[]`                                | -                                 | -               |
-| **initialValue**      | 选择器初始值                 | `string`, `string[]`                                | -                                 | -               |
-| **placeholder**       | 占位文本内容                 | `string`                                            | -                                 | -               |
-| **width**             | 组件的 CSS 宽度值            | `string`                                            | -                                 | `initial`       |
-| **size**              | 选择器组件大小               | `NormalSizes`                                       | [NormalSizes](#normalsizes)       | `medium`        |
-| **icon**              | 右侧图标组件                 | `ComponentType`                                     | -                                 | `SVG Component` |
-| **pure**              | 隐藏右侧图标组件             | `boolean`                                           | -                                 | `false`         |
-| **multiple**          | 是否支持多选                 | `boolean`                                           | -                                 | `false`         |
-| **disabled**          | 禁用所有的交互               | `boolean`                                           | -                                 | `false`         |
-| **onChange**          | 选项被选中所触发的事件       | <Code>(val: string &#124; string[]) => void </Code> | -                                 | -               |
-| **dropdownClassName** | 下拉框的自定义类名           | `string`                                            | -                                 | -               |
-| **dropdownStyle**     | 下拉框的自定义样式           | `object`                                            | -                                 | -               |
-| **disableMatchWidth** | 禁止 Option 跟随单选框的宽度 | `boolean`                                           | -                                 | `false`         |
-| ...                   | 原生属性                     | `HTMLAttributes`                                    | `'name', 'alt', 'className', ...` | -               |
+| 属性                  | 描述                              | 类型                                                | 推荐值                            | 默认            |
+| --------------------- | --------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
+| **value**             | 手动设置选择器的值                | `string`, `string[]`                                | -                                 | -               |
+| **initialValue**      | 选择器初始值                      | `string`, `string[]`                                | -                                 | -               |
+| **placeholder**       | 占位文本内容                      | `string`                                            | -                                 | -               |
+| **width**             | 组件的 CSS 宽度值                 | `string`                                            | -                                 | `initial`       |
+| **size**              | 选择器组件大小                    | `NormalSizes`                                       | [NormalSizes](#normalsizes)       | `medium`        |
+| **icon**              | 右侧图标组件                      | `ComponentType`                                     | -                                 | `SVG Component` |
+| **pure**              | 隐藏右侧图标组件                  | `boolean`                                           | -                                 | `false`         |
+| **multiple**          | 是否支持多选                      | `boolean`                                           | -                                 | `false`         |
+| **disabled**          | 禁用所有的交互                    | `boolean`                                           | -                                 | `false`         |
+| **onChange**          | 选项被选中所触发的事件            | <Code>(val: string &#124; string[]) => void </Code> | -                                 | -               |
+| **dropdownClassName** | 下拉框的自定义类名                | `string`                                            | -                                 | -               |
+| **dropdownStyle**     | 下拉框的自定义样式                | `object`                                            | -                                 | -               |
+| **disableMatchWidth** | 禁止 Option 跟随单选框的宽度      | `boolean`                                           | -                                 | `false`         |
+| **getPopupContainer** | 下拉框渲染的父元素，默认是 `body` | `() => HTMLElement`                                 | -                                 | -               |
+| ...                   | 原生属性                          | `HTMLAttributes`                                    | `'name', 'alt', 'className', ...` | -               |
 
 <Attributes.Title>Select.Option.Props</Attributes.Title>
 
diff --git a/pages/zh-cn/components/snippet.mdx b/pages/zh-cn/components/snippet.mdx
index 2afaace..ee481ad 100644
--- a/pages/zh-cn/components/snippet.mdx
+++ b/pages/zh-cn/components/snippet.mdx
@@ -84,14 +84,17 @@ export const meta = {
 <Attributes edit="/pages/zh-cn/components/snippet.mdx">
 <Attributes.Title>Snippet.Props</Attributes.Title>
 
-| 属性       | 描述               | 类型                          | 推荐值                           | 默认      |
-| ---------- | ------------------ | ----------------------------- | -------------------------------- | --------- |
-| **text**   | 命令文本           | `string` `Array<string>`      | -                                | -         |
-| **type**   | 组件类型           | [SnippetTypes](#snippettypes) | [SnippetTypes](#snippettypes)    | `default` |
-| **filled** | 填充风格的样式     | `boolean`                     | -                                | `false`   |
-| **width**  | 设置 CSS 宽度      | `string`                      | -                                | `initial` |
-| **copy**   | 拷贝按钮的工作方式 | `CopyTypes`                   | `'default', 'slient', 'prevent'` | `default` |
-| ...        | 原生属性           | `HTMLAttributes`              | `'id', 'name', 'className', ...` | -         |
+| 属性          | 描述               | 类型                          | 推荐值                           | 默认                   |
+| ------------- | ------------------ | ----------------------------- | -------------------------------- | ---------------------- |
+| **text**      | 命令文本           | `string` `Array<string>`      | -                                | -                      |
+| **type**      | 组件类型           | [SnippetTypes](#snippettypes) | [SnippetTypes](#snippettypes)    | `default`              |
+| **filled**    | 填充风格的样式     | `boolean`                     | -                                | `false`                |
+| **width**     | 设置 CSS 宽度      | `string`                      | -                                | `initial`              |
+| **copy**      | 拷贝按钮的工作方式 | `CopyTypes`                   | `'default', 'slient', 'prevent'` | `default`              |
+| **symbol**    | 组件左侧显示的字符 | `string`                      | -                                | `$`                    |
+| **toastText** | 拷贝提示的字符     | `string`                      | -                                | `Copied to clipboard!` |
+| **toastType** | 拷贝提示的类型     | `NormalTypes`                 | [NormalTypes](#normaltypes)      | `success`              |
+| ...           | 原生属性           | `HTMLAttributes`              | `'id', 'name', 'className', ...` | -                      |
 
 <Attributes.Title>SnippetTypes</Attributes.Title>
 
@@ -99,6 +102,12 @@ export const meta = {
 type SnippetTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error' | 'dark' | 'lite'
 ```
 
+<Attributes.Title>NormalTypes</Attributes.Title>
+
+```ts
+type NormalTypes = 'default' | 'secondary' | 'success' | 'warning' | 'error'
+```
+
 </Attributes>
 
 export default ({ children }) => <Layout meta={meta}>{children}</Layout>
diff --git a/pages/zh-cn/components/tabs.mdx b/pages/zh-cn/components/tabs.mdx
index 59933ce..30143de 100644
--- a/pages/zh-cn/components/tabs.mdx
+++ b/pages/zh-cn/components/tabs.mdx
@@ -131,7 +131,7 @@ export const meta = {
 | 属性              | 描述           | 类型      | 推荐值 | 默认    |
 | ----------------- | -------------- | --------- | ------ | ------- |
 | **label**(必须的) | 选项卡标签文字 | `string`  | -      | -       |
-| **value**         | 唯一鉴别值     | `string`  | -      | -       |
+| **value**(必须的) | 唯一鉴别值     | `string`  | -      | -       |
 | **disabled**      | 禁用当前选项卡 | `boolean` | -      | `false` |
 
 <Attributes.Title>useTabs</Attributes.Title>
diff --git a/pages/zh-cn/components/textarea.mdx b/pages/zh-cn/components/textarea.mdx
index 758d162..6120338 100644
--- a/pages/zh-cn/components/textarea.mdx
+++ b/pages/zh-cn/components/textarea.mdx
@@ -99,6 +99,29 @@ export const meta = {
 `}
 />
 
+<Playground
+  title="命令式 API"
+  desc="使用非受控方式更新组件。"
+  scope={{ Textarea, Spacer, Button }}
+  code={`
+() => {
+  const ref = React.useRef(null)
+  const setChange = () => {
+    ref && (ref.current.value = Math.random().toString(32))
+  }
+  return (
+    <>
+      <Textarea initialValue="Hello" onChange={e => console.log(e.target.value)} ref={ref} />
+      <Spacer y={.5} />
+      <Button auto type="secondary" size="small"
+        onClick={setChange}>设置值</Button>
+    </>
+  )
+}
+`}
+/>
+
+
 <Attributes edit="/pages/zh-cn/components/textarea.mdx">
 <Attributes.Title alias="Input.Textarea">Textarea.Props</Attributes.Title>
 
diff --git a/pages/zh-cn/components/toast.mdx b/pages/zh-cn/components/toast.mdx
index dd4a7de..267e836 100644
--- a/pages/zh-cn/components/toast.mdx
+++ b/pages/zh-cn/components/toast.mdx
@@ -125,6 +125,17 @@ interface ToastAction {
 }
 ```
 
+<Attributes.Title>NormalTypes</Attributes.Title>
+
+```ts
+type NormalTypes = 'default'
+  | 'secondary'
+  | 'success'
+  | 'warning'
+  | 'error'
+```
+
+
 </Attributes>
 
 export default ({ children }) => <Layout meta={meta}>{children}</Layout>


Test patch:
diff --git a/components/button-group/__tests__/__snapshots__/index.test.tsx.snap b/components/button-group/__tests__/__snapshots__/index.test.tsx.snap
index 06fd2bb..130961b 100644
--- a/components/button-group/__tests__/__snapshots__/index.test.tsx.snap
+++ b/components/button-group/__tests__/__snapshots__/index.test.tsx.snap
@@ -35,7 +35,8 @@ exports[`ButtonGroup buttons should be displayed vertically 1`] = `
             --zeit-ui-button-bg: #fff;
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
             color: #000;
             --zeit-ui-button-color: #000;
             background-color: #fff;
@@ -96,7 +97,8 @@ exports[`ButtonGroup buttons should be displayed vertically 1`] = `
             --zeit-ui-button-bg: #fff;
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
             color: #000;
             --zeit-ui-button-color: #000;
             background-color: #fff;
@@ -205,7 +207,8 @@ exports[`ButtonGroup props should be passed to each button 1`] = `
             --zeit-ui-button-bg: #0070f3;
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
             color: #0070f3;
             --zeit-ui-button-color: #0070f3;
             background-color: #fff;
@@ -314,7 +317,8 @@ exports[`ButtonGroup props should be passed to each button 2`] = `
             --zeit-ui-button-bg: #0070f3;
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
             color: #0070f3;
             --zeit-ui-button-color: #0070f3;
             background-color: #fff;
@@ -423,7 +427,8 @@ exports[`ButtonGroup should render correctly 1`] = `
             --zeit-ui-button-bg: #fff;
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
             color: #000;
             --zeit-ui-button-color: #000;
             background-color: #fff;
diff --git a/components/button/__tests__/__snapshots__/icon.test.tsx.snap b/components/button/__tests__/__snapshots__/icon.test.tsx.snap
index 62c0e08..b0e470b 100644
--- a/components/button/__tests__/__snapshots__/icon.test.tsx.snap
+++ b/components/button/__tests__/__snapshots__/icon.test.tsx.snap
@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`ButtonIcon should render correctly 1`] = `
-"<button type=\\"button\\" class=\\"btn \\"><span class=\\"icon  \\"><svg></svg><style>
+"<button type=\\"button\\" class=\\"btn \\"><span class=\\"icon   \\"><svg></svg><style>
         .icon {
           position: absolute;
           left: var(--zeit-ui-button-padding);
@@ -25,6 +25,11 @@ exports[`ButtonIcon should render correctly 1`] = `
           height: calc(var(--zeit-ui-button-height) / 2.35);
           width: calc(var(--zeit-ui-button-height) / 2.35);
         }
+
+        .single {
+          position: static;
+          transform: none;
+        }
       </style></span><div class=\\"text left\\">action<style>
           .left {
             padding-left: 0;
@@ -66,7 +71,8 @@ exports[`ButtonIcon should render correctly 1`] = `
             --zeit-ui-button-bg: #fff;
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
             color: #000;
             --zeit-ui-button-color: #000;
             background-color: #fff;
@@ -97,7 +103,7 @@ exports[`ButtonIcon should render correctly 1`] = `
 `;
 
 exports[`ButtonIcon should work with right 1`] = `
-"<button type=\\"button\\" class=\\"btn \\"><span class=\\"icon right \\"><svg></svg><style>
+"<button type=\\"button\\" class=\\"btn \\"><span class=\\"icon right  \\"><svg></svg><style>
         .icon {
           position: absolute;
           left: var(--zeit-ui-button-padding);
@@ -121,6 +127,11 @@ exports[`ButtonIcon should work with right 1`] = `
           height: calc(var(--zeit-ui-button-height) / 2.35);
           width: calc(var(--zeit-ui-button-height) / 2.35);
         }
+
+        .single {
+          position: static;
+          transform: none;
+        }
       </style></span><div class=\\"text right\\">action<style>
           .left {
             padding-left: 0;
@@ -162,7 +173,103 @@ exports[`ButtonIcon should work with right 1`] = `
             --zeit-ui-button-bg: #fff;
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
+            color: #000;
+            --zeit-ui-button-color: #000;
+            background-color: #fff;
+            border-color: #000;
+            cursor: pointer;
+            pointer-events: auto;
+            box-shadow: none;
+            transform: translate3d(0px, 0px, 0px);
+          }
+
+          .btn :global(.text) {
+            position: relative;
+            z-index: 1;
+            display: inline-flex;
+            justify-content: center;
+            align-items: center;
+            text-align: center;
+            line-height: inherit;
+            top: -1px;
+          }
+
+          .btn :global(.text p),
+          .btn :global(.text pre),
+          .btn :global(.text div) {
+            margin: 0;
+          }
+        </style></button>"
+`;
+
+exports[`ButtonIcon should work without text 1`] = `
+"<button type=\\"button\\" class=\\"btn \\"><span class=\\"icon right single \\"><svg></svg><style>
+        .icon {
+          position: absolute;
+          left: var(--zeit-ui-button-padding);
+          right: auto;
+          top: 50%;
+          transform: translateY(-50%);
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          color: var(--zeit-ui-button-color);
+          z-index: 1;
+        }
+
+        .right {
+          right: var(--zeit-ui-button-padding);
+          left: auto;
+        }
+
+        .icon :global(svg) {
+          background: transparent;
+          height: calc(var(--zeit-ui-button-height) / 2.35);
+          width: calc(var(--zeit-ui-button-height) / 2.35);
+        }
+
+        .single {
+          position: static;
+          transform: none;
+        }
+      </style></span><style>
+          .btn {
+            box-sizing: border-box;
+            display: inline-block;
+            padding: 0 1.375rem;
+            height: 2.5rem;
+            line-height: 2.5rem;
+            min-width: 12.5rem;
+            width: auto;
+            border-radius: 5px;
+            font-weight: 400;
+            font-size: .875rem;
+            user-select: none;
+            outline: none;
+            text-transform: capitalize;
+            justify-content: center;
+            text-align: center;
+            white-space: nowrap;
+            transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
+              border 200ms ease 0ms, color 200ms ease 0ms;
+            position: relative;
+            overflow: hidden;
+            color: #666;
+            background-color: #fff;
+            border: 1px solid #eaeaea;
+            cursor: pointer;
+            pointer-events: auto;
+            box-shadow: none;
+            --zeit-ui-button-padding: 1.375rem;
+            --zeit-ui-button-height: 2.5rem;
+            --zeit-ui-button-color: #666;
+            --zeit-ui-button-bg: #fff;
+          }
+
+          .btn:hover,
+          .btn:focus {
             color: #000;
             --zeit-ui-button-color: #000;
             background-color: #fff;
diff --git a/components/button/__tests__/icon.test.tsx b/components/button/__tests__/icon.test.tsx
index 02470d6..b443b06 100644
--- a/components/button/__tests__/icon.test.tsx
+++ b/components/button/__tests__/icon.test.tsx
@@ -16,6 +16,13 @@ describe('ButtonIcon', () => {
     expect(() => wrapper.unmount()).not.toThrow()
   })
 
+  it('should work without text', () => {
+    const wrapper = mount(<Button iconRight={<Icon />} />)
+    const text = wrapper.find('.text')
+    expect(wrapper.html()).toMatchSnapshot()
+    expect(text.length).toBe(0)
+  })
+
   it('the width of the text should be filled', () => {
     const autoWrapper = mount(
       <Button auto icon={<Icon />}>
diff --git a/components/image/__tests__/browser.test.tsx b/components/image/__tests__/browser.test.tsx
index db6c828..b863a16 100644
--- a/components/image/__tests__/browser.test.tsx
+++ b/components/image/__tests__/browser.test.tsx
@@ -68,6 +68,6 @@ describe('Image Browser', () => {
       </Image.Browser>,
     )
     const rel = wrapper.find('a').getDOMNode().getAttribute('rel')
-    expect(anchorRel).toEqual(anchorRel)
+    expect(rel).toEqual(anchorRel)
   })
 })
diff --git a/components/link/__tests__/__snapshots__/index.test.tsx.snap b/components/link/__tests__/__snapshots__/index.test.tsx.snap
index b9f6c0d..5054760 100644
--- a/components/link/__tests__/__snapshots__/index.test.tsx.snap
+++ b/components/link/__tests__/__snapshots__/index.test.tsx.snap
@@ -120,7 +120,7 @@ exports[`Link should render correctly 1`] = `
           }
 
           .link:hover {
-            background-color: #0076ff1a;
+            background-color: rgba(0, 112, 243, 0.1);
             color: #3291ff;
           }
         </style></a></div>"
diff --git a/components/modal/__tests__/__snapshots__/index.test.tsx.snap b/components/modal/__tests__/__snapshots__/index.test.tsx.snap
index df1c29c..9fe2600 100644
--- a/components/modal/__tests__/__snapshots__/index.test.tsx.snap
+++ b/components/modal/__tests__/__snapshots__/index.test.tsx.snap
@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Modal customization should be supported 1`] = `
-"<div class=\\"wrapper test-class  wrapper-enter\\"><h2 class=\\"\\">Modal</h2><style>
+"<div class=\\"wrapper test-class  wrapper-enter\\" role=\\"dialog\\" tabindex=\\"-1\\"><div tabindex=\\"0\\" class=\\"hide-tab\\" aria-hidden=\\"true\\"></div><h2 class=\\"\\">Modal</h2><style>
         h2 {
           font-size: 1.5rem;
           line-height: 1.6;
@@ -16,10 +16,10 @@ exports[`Modal customization should be supported 1`] = `
           text-transform: capitalize;
           color: #000;
         }
-      </style><style>
+      </style><div tabindex=\\"0\\" class=\\"hide-tab\\" aria-hidden=\\"true\\"></div><style>
           .wrapper {
-            max-width: 85vw;
-            max-height: 75vh;
+            max-width: 90vw;
+            max-height: 90vh;
             width: 100px;
             overflow: hidden;
             display: flex;
@@ -32,14 +32,15 @@ exports[`Modal customization should be supported 1`] = `
             padding: 16pt;
             box-shadow: 0 30px 60px rgba(0, 0, 0, 0.12);
             opacity: 0;
-            transform: translate3d(0px, -40px, 0px);
+            outline: none;
+            transform: translate3d(0px, -30px, 0px);
             transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s,
               transform 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
           }
 
           .wrapper-enter {
             opacity: 0;
-            transform: translate3d(0px, -40px, 0px);
+            transform: translate3d(0px, -30px, 0px);
           }
 
           .wrapper-enter-active {
@@ -54,13 +55,21 @@ exports[`Modal customization should be supported 1`] = `
 
           .wrapper-leave-active {
             opacity: 0;
-            transform: translate3d(0px, -50px, 0px);
+            transform: translate3d(0px, -30px, 0px);
+          }
+
+          .hide-tab {
+            outline: none;
+            overflow: hidden;
+            width: 0;
+            height: 0;
+            opacity: 0;
           }
         </style></div>"
 `;
 
 exports[`Modal should render correctly 1`] = `
-"<div class=\\"backdrop  transition-enter\\"><div class=\\"layer\\"></div><div class=\\"content\\"><div class=\\"wrapper   wrapper-enter\\"><h2 class=\\"\\">Modal</h2><style>
+"<div class=\\"backdrop  backdrop-wrapper-enter\\"><div class=\\"layer\\"></div><div class=\\"content\\"><div class=\\"wrapper   wrapper-enter\\" role=\\"dialog\\" tabindex=\\"-1\\"><div tabindex=\\"0\\" class=\\"hide-tab\\" aria-hidden=\\"true\\"></div><h2 class=\\"\\">Modal</h2><style>
         h2 {
           font-size: 1.5rem;
           line-height: 1.6;
@@ -94,6 +103,7 @@ exports[`Modal should render correctly 1`] = `
           margin: 0 -16pt;
           padding: 16pt 16pt 8pt;
           overflow-y: auto;
+          position: relative;
         }
 
         .content > :global(*:first-child) {
@@ -103,53 +113,131 @@ exports[`Modal should render correctly 1`] = `
         .content > :global(*:last-child) {
           margin-bottom: 0;
         }
-      </style><div></div><footer><button class=\\"\\">Cancel</button><style>
-        button {
-          font-size: 0.75rem;
-          text-transform: uppercase;
-          display: flex;
-          -webkit-box-align: center;
-          align-items: center;
-          -webkit-box-pack: center;
-          justify-content: center;
-          outline: none;
-          text-decoration: none;
-          transition: all 200ms ease-in-out 0s;
-          border: none;
-          color: #666;
-          background-color: #fff;
-          cursor: pointer;
-          flex: 1;
-        }
+      </style><div></div><footer><button type=\\"button\\" class=\\"btn mock \\"><div class=\\"text\\">Cancel</div><style>
+          .btn {
+            box-sizing: border-box;
+            display: inline-block;
+            padding: 0 1.375rem;
+            height: 2.5rem;
+            line-height: 2.5rem;
+            min-width: 12.5rem;
+            width: auto;
+            border-radius: 5px;
+            font-weight: 400;
+            font-size: .875rem;
+            user-select: none;
+            outline: none;
+            text-transform: capitalize;
+            justify-content: center;
+            text-align: center;
+            white-space: nowrap;
+            transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
+              border 200ms ease 0ms, color 200ms ease 0ms;
+            position: relative;
+            overflow: hidden;
+            color: #666;
+            background-color: #fff;
+            border: 1px solid #eaeaea;
+            cursor: pointer;
+            pointer-events: auto;
+            box-shadow: none;
+            --zeit-ui-button-padding: 1.375rem;
+            --zeit-ui-button-height: 2.5rem;
+            --zeit-ui-button-color: #666;
+            --zeit-ui-button-bg: #fff;
+          }
 
-        button:hover {
-          color: #000;
-          background-color: #fafafa;
-        }
-      </style><button class=\\"\\">Submit</button><style>
-        button {
-          font-size: 0.75rem;
-          text-transform: uppercase;
-          display: flex;
-          -webkit-box-align: center;
-          align-items: center;
-          -webkit-box-pack: center;
-          justify-content: center;
-          outline: none;
-          text-decoration: none;
-          transition: all 200ms ease-in-out 0s;
-          border: none;
-          color: #000;
-          background-color: #fff;
-          cursor: pointer;
-          flex: 1;
-        }
+          .btn:hover,
+          .btn:focus {
+            color: #000;
+            --zeit-ui-button-color: #000;
+            background-color: #fff;
+            border-color: #000;
+            cursor: pointer;
+            pointer-events: auto;
+            box-shadow: none;
+            transform: translate3d(0px, 0px, 0px);
+          }
 
-        button:hover {
-          color: #000;
-          background-color: #fafafa;
-        }
-      </style></footer><style>
+          .btn :global(.text) {
+            position: relative;
+            z-index: 1;
+            display: inline-flex;
+            justify-content: center;
+            align-items: center;
+            text-align: center;
+            line-height: inherit;
+            top: -1px;
+          }
+
+          .btn :global(.text p),
+          .btn :global(.text pre),
+          .btn :global(.text div) {
+            margin: 0;
+          }
+        </style></button><button type=\\"button\\" class=\\"btn mock \\"><div class=\\"text\\">Submit</div><style>
+          .btn {
+            box-sizing: border-box;
+            display: inline-block;
+            padding: 0 1.375rem;
+            height: 2.5rem;
+            line-height: 2.5rem;
+            min-width: 12.5rem;
+            width: auto;
+            border-radius: 5px;
+            font-weight: 400;
+            font-size: .875rem;
+            user-select: none;
+            outline: none;
+            text-transform: capitalize;
+            justify-content: center;
+            text-align: center;
+            white-space: nowrap;
+            transition: background-color 200ms ease 0ms, box-shadow 200ms ease 0ms,
+              border 200ms ease 0ms, color 200ms ease 0ms;
+            position: relative;
+            overflow: hidden;
+            color: #666;
+            background-color: #fff;
+            border: 1px solid #eaeaea;
+            cursor: pointer;
+            pointer-events: auto;
+            box-shadow: none;
+            --zeit-ui-button-padding: 1.375rem;
+            --zeit-ui-button-height: 2.5rem;
+            --zeit-ui-button-color: #666;
+            --zeit-ui-button-bg: #fff;
+          }
+
+          .btn:hover,
+          .btn:focus {
+            color: #000;
+            --zeit-ui-button-color: #000;
+            background-color: #fff;
+            border-color: #000;
+            cursor: pointer;
+            pointer-events: auto;
+            box-shadow: none;
+            transform: translate3d(0px, 0px, 0px);
+          }
+
+          .btn :global(.text) {
+            position: relative;
+            z-index: 1;
+            display: inline-flex;
+            justify-content: center;
+            align-items: center;
+            text-align: center;
+            line-height: inherit;
+            top: -1px;
+          }
+
+          .btn :global(.text p),
+          .btn :global(.text pre),
+          .btn :global(.text div) {
+            margin: 0;
+          }
+        </style></button></footer><style>
         footer {
           display: flex;
           overflow: hidden;
@@ -164,7 +252,7 @@ exports[`Modal should render correctly 1`] = `
           border-bottom-right-radius: 5px;
         }
 
-        footer > :global(button + button) {
+        footer > :global(button.btn + button.btn) {
           border-left: 1px solid #eaeaea;
         }
 
@@ -172,10 +260,10 @@ exports[`Modal should render correctly 1`] = `
           height: 3.625rem;
           flex-shrink: 0;
         }
-      </style><style>
+      </style><div tabindex=\\"0\\" class=\\"hide-tab\\" aria-hidden=\\"true\\"></div><style>
           .wrapper {
-            max-width: 85vw;
-            max-height: 75vh;
+            max-width: 90vw;
+            max-height: 90vh;
             width: 26rem;
             overflow: hidden;
             display: flex;
@@ -188,14 +276,15 @@ exports[`Modal should render correctly 1`] = `
             padding: 16pt;
             box-shadow: 0 30px 60px rgba(0, 0, 0, 0.12);
             opacity: 0;
-            transform: translate3d(0px, -40px, 0px);
+            outline: none;
+            transform: translate3d(0px, -30px, 0px);
             transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s,
               transform 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
           }
 
           .wrapper-enter {
             opacity: 0;
-            transform: translate3d(0px, -40px, 0px);
+            transform: translate3d(0px, -30px, 0px);
           }
 
           .wrapper-enter-active {
@@ -210,7 +299,15 @@ exports[`Modal should render correctly 1`] = `
 
           .wrapper-leave-active {
             opacity: 0;
-            transform: translate3d(0px, -50px, 0px);
+            transform: translate3d(0px, -30px, 0px);
+          }
+
+          .hide-tab {
+            outline: none;
+            overflow: hidden;
+            width: 0;
+            height: 0;
+            opacity: 0;
           }
         </style></div></div><div class=\\"offset\\"></div><style>
             .backdrop {
@@ -221,7 +318,7 @@ exports[`Modal should render correctly 1`] = `
               align-content: center;
               align-items: center;
               flex-direction: column;
-              justify-content: space-around;
+              justify-content: center;
               height: 100vh;
               width: 100vw;
               overflow: auto;
@@ -258,5 +355,21 @@ exports[`Modal should render correctly 1`] = `
               pointer-events: none;
               z-index: 1000;
             }
+
+            .backdrop-wrapper-enter .layer {
+              opacity: 0;
+            }
+
+            .backdrop-wrapper-enter-active .layer {
+              opacity: 0.25;
+            }
+
+            .backdrop-wrapper-leave .layer {
+              opacity: 0.25;
+            }
+
+            .backdrop-wrapper-leave-active .layer {
+              opacity: 0;
+            }
           </style></div>"
 `;
diff --git a/components/modal/__tests__/index.test.tsx b/components/modal/__tests__/index.test.tsx
index d73d135..7e6d2c3 100644
--- a/components/modal/__tests__/index.test.tsx
+++ b/components/modal/__tests__/index.test.tsx
@@ -3,6 +3,13 @@ import { mount } from 'enzyme'
 import { Modal } from 'components'
 import { nativeEvent, updateWrapper } from 'tests/utils'
 import { expectModalIsClosed, expectModalIsOpened } from './use-modal.test'
+import { act } from 'react-dom/test-utils'
+
+const TabEvent = {
+  key: 'TAB',
+  keyCode: 9,
+  which: 9,
+}
 
 describe('Modal', () => {
   it('should render correctly', () => {
@@ -117,4 +124,32 @@ describe('Modal', () => {
     expect(html).toContain('test-class')
     expect(() => wrapper.unmount()).not.toThrow()
   })
+
+  it('focus should only be switched within modal', () => {
+    const wrapper = mount(
+      <Modal open={true} width="100px" wrapClassName="test-class">
+        <Modal.Title>Modal</Modal.Title>
+      </Modal>,
+    )
+    const tabStart = wrapper.find('.hide-tab').at(0).getDOMNode()
+    const tabEnd = wrapper.find('.hide-tab').at(1).getDOMNode()
+    const eventElement = wrapper.find('.wrapper').at(0)
+    expect(document.activeElement).toBe(tabStart)
+
+    act(() => {
+      eventElement.simulate('keydown', {
+        ...TabEvent,
+        shiftKey: true,
+      })
+    })
+    expect(document.activeElement).toBe(tabEnd)
+
+    act(() => {
+      eventElement.simulate('keydown', {
+        ...TabEvent,
+        shiftKey: false,
+      })
+    })
+    expect(document.activeElement).toBe(tabStart)
+  })
 })
diff --git a/components/shared/__tests__/__snapshots__/backdrop.test.tsx.snap b/components/shared/__tests__/__snapshots__/backdrop.test.tsx.snap
index e2c6c04..0bb79d4 100644
--- a/components/shared/__tests__/__snapshots__/backdrop.test.tsx.snap
+++ b/components/shared/__tests__/__snapshots__/backdrop.test.tsx.snap
@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Backdrop should render correctly 1`] = `
-"<div class=\\"backdrop  transition-enter\\"><div class=\\"layer\\"></div><div class=\\"content\\"><span>test-value</span></div><div class=\\"offset\\"></div><style>
+"<div class=\\"backdrop  backdrop-wrapper-enter\\"><div class=\\"layer\\"></div><div class=\\"content\\"><span>test-value</span></div><div class=\\"offset\\"></div><style>
             .backdrop {
               position: fixed;
               top: 0;
@@ -10,7 +10,7 @@ exports[`Backdrop should render correctly 1`] = `
               align-content: center;
               align-items: center;
               flex-direction: column;
-              justify-content: space-around;
+              justify-content: center;
               height: 100vh;
               width: 100vw;
               overflow: auto;
@@ -47,5 +47,21 @@ exports[`Backdrop should render correctly 1`] = `
               pointer-events: none;
               z-index: 1000;
             }
+
+            .backdrop-wrapper-enter .layer {
+              opacity: 0;
+            }
+
+            .backdrop-wrapper-enter-active .layer {
+              opacity: 0.25;
+            }
+
+            .backdrop-wrapper-leave .layer {
+              opacity: 0.25;
+            }
+
+            .backdrop-wrapper-leave-active .layer {
+              opacity: 0;
+            }
           </style></div>"
 `;
diff --git a/components/shared/__tests__/dropdown.test.tsx b/components/shared/__tests__/dropdown.test.tsx
index 95f2507..49b39b5 100644
--- a/components/shared/__tests__/dropdown.test.tsx
+++ b/components/shared/__tests__/dropdown.test.tsx
@@ -164,4 +164,24 @@ describe('Dropdown', () => {
 
     expect(() => wrapper.unmount()).not.toThrow()
   })
+
+  it('should render to specified container', () => {
+    const Mock: React.FC<{}> = () => {
+      const ref = useRef<HTMLDivElement>(null)
+      const customContainer = useRef<HTMLDivElement>(null)
+      return (
+        <div>
+          <div ref={customContainer} id="custom" />
+          <div ref={ref}>
+            <Dropdown parent={ref} visible getPopupContainer={() => customContainer.current}>
+              <span>test-value</span>
+            </Dropdown>
+          </div>
+        </div>
+      )
+    }
+    const wrapper = mount(<Mock />)
+    const customContainer = wrapper.find('#custom')
+    expect(customContainer.html()).toContain('dropdown')
+  })
 })
diff --git a/components/snippet/__tests__/__snapshots__/index.test.tsx.snap b/components/snippet/__tests__/__snapshots__/index.test.tsx.snap
index 5a7ff70..5212bc3 100644
--- a/components/snippet/__tests__/__snapshots__/index.test.tsx.snap
+++ b/components/snippet/__tests__/__snapshots__/index.test.tsx.snap
@@ -58,6 +58,122 @@ exports[`Snippet should render correctly 1`] = `
       </style></div>"
 `;
 
+exports[`Snippet should work with custom symbol 1`] = `
+"<div class=\\"snippet \\"><pre>yarn add @zeit-ui/react</pre><div class=\\"copy\\"><svg viewBox=\\"0 0 24 24\\" width=\\"22\\" height=\\"22\\" stroke=\\"currentColor\\" stroke-width=\\"1.5\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\" style=\\"color: currentcolor;\\"><path d=\\"M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z\\"></path></svg></div><style>
+        .snippet {
+          position: relative;
+          width: initial;
+          max-width: 100%;
+          padding: 8pt;
+          padding-right: calc(2 * 16pt);
+          color: #000;
+          background-color: #fff;
+          border: 1px solid #eaeaea;
+          border-radius: 5px;
+        }
+
+        pre {
+          margin: 0;
+          padding: 0;
+          border: none;
+          background-color: transparent;
+          color: #000;
+          font-size: 0.8125rem;
+        }
+
+        pre::before {
+          content: '> ';
+          user-select: none;
+        }
+
+        pre :global(*) {
+          margin: 0;
+          padding: 0;
+          font-size: inherit;
+          color: inherit;
+        }
+
+        .copy {
+          position: absolute;
+          right: 0;
+          top: -2px;
+          transform: translateY(50%);
+          background-color: #fff;
+          display: inline-flex;
+          justify-content: center;
+          align-items: center;
+          width: calc(2 * 16pt);
+          color: inherit;
+          transition: opacity 0.2s ease 0s;
+          border-radius: 5px;
+          cursor: pointer;
+          user-select: none;
+        }
+
+        .copy:hover {
+          opacity: 0.7;
+        }
+      </style></div>"
+`;
+
+exports[`Snippet should work with custom symbol 2`] = `
+"<div class=\\"snippet \\"><pre>yarn add @zeit-ui/react</pre><div class=\\"copy\\"><svg viewBox=\\"0 0 24 24\\" width=\\"22\\" height=\\"22\\" stroke=\\"currentColor\\" stroke-width=\\"1.5\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\" style=\\"color: currentcolor;\\"><path d=\\"M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z\\"></path></svg></div><style>
+        .snippet {
+          position: relative;
+          width: initial;
+          max-width: 100%;
+          padding: 8pt;
+          padding-right: calc(2 * 16pt);
+          color: #000;
+          background-color: #fff;
+          border: 1px solid #eaeaea;
+          border-radius: 5px;
+        }
+
+        pre {
+          margin: 0;
+          padding: 0;
+          border: none;
+          background-color: transparent;
+          color: #000;
+          font-size: 0.8125rem;
+        }
+
+        pre::before {
+          content: '';
+          user-select: none;
+        }
+
+        pre :global(*) {
+          margin: 0;
+          padding: 0;
+          font-size: inherit;
+          color: inherit;
+        }
+
+        .copy {
+          position: absolute;
+          right: 0;
+          top: -2px;
+          transform: translateY(50%);
+          background-color: #fff;
+          display: inline-flex;
+          justify-content: center;
+          align-items: center;
+          width: calc(2 * 16pt);
+          color: inherit;
+          transition: opacity 0.2s ease 0s;
+          border-radius: 5px;
+          cursor: pointer;
+          user-select: none;
+        }
+
+        .copy:hover {
+          opacity: 0.7;
+        }
+      </style></div>"
+`;
+
 exports[`Snippet should work with different styles 1`] = `
 "<div><div class=\\"snippet \\"><pre>yarn add @zeit-ui/react</pre><div class=\\"copy\\"><svg viewBox=\\"0 0 24 24\\" width=\\"22\\" height=\\"22\\" stroke=\\"currentColor\\" stroke-width=\\"1.5\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\" style=\\"color: currentcolor;\\"><path d=\\"M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z\\"></path></svg></div><style>
         .snippet {
diff --git a/components/snippet/__tests__/index.test.tsx b/components/snippet/__tests__/index.test.tsx
index 7751caa..3265981 100644
--- a/components/snippet/__tests__/index.test.tsx
+++ b/components/snippet/__tests__/index.test.tsx
@@ -70,6 +70,29 @@ describe('Snippet', () => {
     expect(wrapper.find('.copy').length).toBe(0)
   })
 
+  it('should work with custom symbol', () => {
+    const wrapper = mount(<Snippet text={command} symbol={'>'} />)
+    expect(wrapper.html()).toMatchSnapshot()
+    expect(() => wrapper.unmount()).not.toThrow()
+
+    const emptySymbolWrapper = mount(<Snippet text={command} symbol=" " />)
+    expect(emptySymbolWrapper.html()).toMatchSnapshot()
+
+    const emptySymbolWrapper2 = mount(<Snippet text={command} symbol="" />)
+    expect(emptySymbolWrapper2.html()).toEqual(emptySymbolWrapper.html())
+
+    expect(() => emptySymbolWrapper.unmount()).not.toThrow()
+    expect(() => emptySymbolWrapper2.unmount()).not.toThrow()
+  })
+
+  it('should work with custom toast', () => {
+    document.execCommand = jest.fn()
+    const wrapper = mount(<Snippet text={command} toastText="Code copied!" toastType="secondary" />)
+    wrapper.find('.copy').simulate('click')
+    expect(document.execCommand).toHaveBeenCalled()
+    ;(document.execCommand as jest.Mock).mockRestore()
+  })
+
   afterAll(() => {
     ;(window.getSelection as jest.Mock).mockRestore()
     ;(document.createRange as jest.Mock).mockRestore()
diff --git a/components/table/__tests__/index.test.tsx b/components/table/__tests__/index.test.tsx
index 581f4cc..2457046 100644
--- a/components/table/__tests__/index.test.tsx
+++ b/components/table/__tests__/index.test.tsx
@@ -3,6 +3,7 @@ import { mount } from 'enzyme'
 import { Table, Code } from 'components'
 import { cellActions } from 'components/table/table-cell'
 import { nativeEvent, updateWrapper } from 'tests/utils'
+import { act } from 'react-dom/test-utils'
 
 const data = [
   { property: 'type', description: 'Content type', default: '-' },
@@ -166,4 +167,40 @@ describe('Table', () => {
     expect(wrapper.find('thead').find('code').length).not.toBe(0)
     expect(wrapper.html()).toMatchSnapshot()
   })
+
+  it('the changes of column should be tracked', () => {
+    const Mock = ({ label }: { label: string }) => {
+      return (
+        <Table data={data}>
+          <Table.Column prop="description" label={label} />
+        </Table>
+      )
+    }
+    const wrapper = mount(<Mock label="test1" />)
+    expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test1')
+
+    act(() => {
+      wrapper.setProps({ label: 'test2' })
+    })
+    expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test2')
+    expect(() => wrapper.unmount()).not.toThrow()
+  })
+
+  it('the changes of children should be tracked', () => {
+    const Mock = ({ label }: { label: string }) => {
+      return (
+        <Table data={data}>
+          <Table.Column prop="description">{label}</Table.Column>
+        </Table>
+      )
+    }
+    const wrapper = mount(<Mock label="test1" />)
+    expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test1')
+
+    act(() => {
+      wrapper.setProps({ label: 'test2' })
+    })
+    expect(wrapper.find('thead').find('tr').at(0).text()).toBe('test2')
+    expect(() => wrapper.unmount()).not.toThrow()
+  })
 })
diff --git a/components/tabs/__tests__/index.test.tsx b/components/tabs/__tests__/index.test.tsx
index 102711a..aa23de6 100644
--- a/components/tabs/__tests__/index.test.tsx
+++ b/components/tabs/__tests__/index.test.tsx
@@ -87,36 +87,26 @@ describe('Tabs', () => {
     expect(active.text()).toContain('label2')
   })
 
-  it('should warning when label duplicated', () => {
-    const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
-    mount(
-      <Tabs>
-        <Tabs.Item label="label1" value="1">
-          test-1
-        </Tabs.Item>
-        <Tabs.Item label="label2" value="1">
-          test-2
-        </Tabs.Item>
-      </Tabs>,
-    )
-    expect(errorSpy).toHaveBeenCalled()
-    errorSpy.mockRestore()
-  })
-
-  it('should use label as key when value is missing', async () => {
-    const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
-    const wrapper = mount(
-      <Tabs>
-        <Tabs.Item label="label1">test-1</Tabs.Item>
-        <Tabs.Item label="label2">test-2</Tabs.Item>
-      </Tabs>,
-    )
-    expect(errorSpy).not.toHaveBeenCalled()
+  it('should re-render when items updated', () => {
+    const Mock = ({ label = 'label1' }) => {
+      return (
+        <Tabs value="1">
+          <Tabs.Item label={label} value="1">
+            test-1
+          </Tabs.Item>
+          <Tabs.Item label="label-fixed" value="2">
+            test-label-fixed
+          </Tabs.Item>
+        </Tabs>
+      )
+    }
+    const wrapper = mount(<Mock />)
+    let active = wrapper.find('header').find('.active')
+    expect(active.text()).toContain('label1')
 
-    wrapper.setProps({ value: 'label2' })
-    await updateWrapper(wrapper, 350)
-    const active = wrapper.find('header').find('.active')
+    wrapper.setProps({ label: 'label2' })
+    active = wrapper.find('header').find('.active')
     expect(active.text()).toContain('label2')
-    errorSpy.mockRestore()
+    expect(() => wrapper.unmount()).not.toThrow()
   })
 })
diff --git a/components/textarea/__tests__/__snapshots__/index.test.tsx.snap b/components/textarea/__tests__/__snapshots__/index.test.tsx.snap
index 328bdc6..5c1716c 100644
--- a/components/textarea/__tests__/__snapshots__/index.test.tsx.snap
+++ b/components/textarea/__tests__/__snapshots__/index.test.tsx.snap
@@ -2,208 +2,208 @@
 
 exports[`Textarea should render correctly 1`] = `
 "<div class=\\"wrapper   \\"><textarea placeholder=\\"placeholder\\"></textarea><style>
-        .wrapper {
-          display: inline-flex;
-          box-sizing: border-box;
-          user-select: none;
-          width: initial;
-          min-width: 12.5rem;
-          max-width: 95vw;
-          height: auto;
-          border-radius: 5px;
-          border: 1px solid #eaeaea;
-          color: #000;
-          transition: border 0.2s ease 0s, color 0.2s ease 0s;
-        }
-
-        .wrapper.hover {
-          border-color: #666;
-        }
-
-        .wrapper.disabled {
-          background-color: #fafafa;
-          border-color: #eaeaea;
-          cursor: not-allowed;
-        }
-
-        textarea {
-          background-color: transparent;
-          box-shadow: none;
-          display: block;
-          font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
-          font-size: 0.875rem;
-          width: 100%;
-          height: 100%;
-          min-height: 6.25rem;
-          resize: none;
-          border: none;
-          outline: none;
-          padding: 8pt;
-        }
-
-        .disabled > textarea {
-          cursor: not-allowed;
-        }
-
-        textarea:-webkit-autofill,
-        textarea:-webkit-autofill:hover,
-        textarea:-webkit-autofill:active,
-        textarea:-webkit-autofill:focus {
-          -webkit-box-shadow: 0 0 0 30px #fff inset !important;
-        }
-      </style></div>"
+          .wrapper {
+            display: inline-flex;
+            box-sizing: border-box;
+            user-select: none;
+            width: initial;
+            min-width: 12.5rem;
+            max-width: 95vw;
+            height: auto;
+            border-radius: 5px;
+            border: 1px solid #eaeaea;
+            color: #000;
+            transition: border 0.2s ease 0s, color 0.2s ease 0s;
+          }
+
+          .wrapper.hover {
+            border-color: #666;
+          }
+
+          .wrapper.disabled {
+            background-color: #fafafa;
+            border-color: #eaeaea;
+            cursor: not-allowed;
+          }
+
+          textarea {
+            background-color: transparent;
+            box-shadow: none;
+            display: block;
+            font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
+            font-size: 0.875rem;
+            width: 100%;
+            height: 100%;
+            min-height: 6.25rem;
+            resize: none;
+            border: none;
+            outline: none;
+            padding: 8pt;
+          }
+
+          .disabled > textarea {
+            cursor: not-allowed;
+          }
+
+          textarea:-webkit-autofill,
+          textarea:-webkit-autofill:hover,
+          textarea:-webkit-autofill:active,
+          textarea:-webkit-autofill:focus {
+            -webkit-box-shadow: 0 0 0 30px #fff inset !important;
+          }
+        </style></div>"
 `;
 
 exports[`Textarea should work with different styles 1`] = `
 "<div><div class=\\"wrapper   \\"><textarea></textarea><style>
-        .wrapper {
-          display: inline-flex;
-          box-sizing: border-box;
-          user-select: none;
-          width: initial;
-          min-width: 12.5rem;
-          max-width: 95vw;
-          height: auto;
-          border-radius: 5px;
-          border: 1px solid #666;
-          color: #000;
-          transition: border 0.2s ease 0s, color 0.2s ease 0s;
-        }
-
-        .wrapper.hover {
-          border-color: #666;
-        }
-
-        .wrapper.disabled {
-          background-color: #fafafa;
-          border-color: #eaeaea;
-          cursor: not-allowed;
-        }
-
-        textarea {
-          background-color: transparent;
-          box-shadow: none;
-          display: block;
-          font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
-          font-size: 0.875rem;
-          width: 100%;
-          height: 100%;
-          min-height: 6.25rem;
-          resize: none;
-          border: none;
-          outline: none;
-          padding: 8pt;
-        }
-
-        .disabled > textarea {
-          cursor: not-allowed;
-        }
-
-        textarea:-webkit-autofill,
-        textarea:-webkit-autofill:hover,
-        textarea:-webkit-autofill:active,
-        textarea:-webkit-autofill:focus {
-          -webkit-box-shadow: 0 0 0 30px #fff inset !important;
-        }
-      </style></div><div class=\\"wrapper   \\"><textarea></textarea><style>
-        .wrapper {
-          display: inline-flex;
-          box-sizing: border-box;
-          user-select: none;
-          width: 20%;
-          min-width: 12.5rem;
-          max-width: 95vw;
-          height: auto;
-          border-radius: 5px;
-          border: 1px solid #eaeaea;
-          color: #000;
-          transition: border 0.2s ease 0s, color 0.2s ease 0s;
-        }
-
-        .wrapper.hover {
-          border-color: #666;
-        }
-
-        .wrapper.disabled {
-          background-color: #fafafa;
-          border-color: #eaeaea;
-          cursor: not-allowed;
-        }
-
-        textarea {
-          background-color: transparent;
-          box-shadow: none;
-          display: block;
-          font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
-          font-size: 0.875rem;
-          width: 100%;
-          height: 100%;
-          min-height: 6.25rem;
-          resize: none;
-          border: none;
-          outline: none;
-          padding: 8pt;
-        }
-
-        .disabled > textarea {
-          cursor: not-allowed;
-        }
-
-        textarea:-webkit-autofill,
-        textarea:-webkit-autofill:hover,
-        textarea:-webkit-autofill:active,
-        textarea:-webkit-autofill:focus {
-          -webkit-box-shadow: 0 0 0 30px #fff inset !important;
-        }
-      </style></div><div class=\\"wrapper   \\"><textarea></textarea><style>
-        .wrapper {
-          display: inline-flex;
-          box-sizing: border-box;
-          user-select: none;
-          width: initial;
-          min-width: 12.5rem;
-          max-width: 95vw;
-          height: auto;
-          border-radius: 5px;
-          border: 1px solid #eaeaea;
-          color: #000;
-          transition: border 0.2s ease 0s, color 0.2s ease 0s;
-        }
-
-        .wrapper.hover {
-          border-color: #666;
-        }
-
-        .wrapper.disabled {
-          background-color: #fafafa;
-          border-color: #eaeaea;
-          cursor: not-allowed;
-        }
-
-        textarea {
-          background-color: transparent;
-          box-shadow: none;
-          display: block;
-          font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
-          font-size: 0.875rem;
-          width: 100%;
-          height: 100%;
-          min-height: 100px;
-          resize: none;
-          border: none;
-          outline: none;
-          padding: 8pt;
-        }
-
-        .disabled > textarea {
-          cursor: not-allowed;
-        }
-
-        textarea:-webkit-autofill,
-        textarea:-webkit-autofill:hover,
-        textarea:-webkit-autofill:active,
-        textarea:-webkit-autofill:focus {
-          -webkit-box-shadow: 0 0 0 30px #fff inset !important;
-        }
-      </style></div></div>"
+          .wrapper {
+            display: inline-flex;
+            box-sizing: border-box;
+            user-select: none;
+            width: initial;
+            min-width: 12.5rem;
+            max-width: 95vw;
+            height: auto;
+            border-radius: 5px;
+            border: 1px solid #666;
+            color: #000;
+            transition: border 0.2s ease 0s, color 0.2s ease 0s;
+          }
+
+          .wrapper.hover {
+            border-color: #666;
+          }
+
+          .wrapper.disabled {
+            background-color: #fafafa;
+            border-color: #eaeaea;
+            cursor: not-allowed;
+          }
+
+          textarea {
+            background-color: transparent;
+            box-shadow: none;
+            display: block;
+            font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
+            font-size: 0.875rem;
+            width: 100%;
+            height: 100%;
+            min-height: 6.25rem;
+            resize: none;
+            border: none;
+            outline: none;
+            padding: 8pt;
+          }
+
+          .disabled > textarea {
+            cursor: not-allowed;
+          }
+
+          textarea:-webkit-autofill,
+          textarea:-webkit-autofill:hover,
+          textarea:-webkit-autofill:active,
+          textarea:-webkit-autofill:focus {
+            -webkit-box-shadow: 0 0 0 30px #fff inset !important;
+          }
+        </style></div><div class=\\"wrapper   \\"><textarea></textarea><style>
+          .wrapper {
+            display: inline-flex;
+            box-sizing: border-box;
+            user-select: none;
+            width: 20%;
+            min-width: 12.5rem;
+            max-width: 95vw;
+            height: auto;
+            border-radius: 5px;
+            border: 1px solid #eaeaea;
+            color: #000;
+            transition: border 0.2s ease 0s, color 0.2s ease 0s;
+          }
+
+          .wrapper.hover {
+            border-color: #666;
+          }
+
+          .wrapper.disabled {
+            background-color: #fafafa;
+            border-color: #eaeaea;
+            cursor: not-allowed;
+          }
+
+          textarea {
+            background-color: transparent;
+            box-shadow: none;
+            display: block;
+            font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
+            font-size: 0.875rem;
+            width: 100%;
+            height: 100%;
+            min-height: 6.25rem;
+            resize: none;
+            border: none;
+            outline: none;
+            padding: 8pt;
+          }
+
+          .disabled > textarea {
+            cursor: not-allowed;
+          }
+
+          textarea:-webkit-autofill,
+          textarea:-webkit-autofill:hover,
+          textarea:-webkit-autofill:active,
+          textarea:-webkit-autofill:focus {
+            -webkit-box-shadow: 0 0 0 30px #fff inset !important;
+          }
+        </style></div><div class=\\"wrapper   \\"><textarea></textarea><style>
+          .wrapper {
+            display: inline-flex;
+            box-sizing: border-box;
+            user-select: none;
+            width: initial;
+            min-width: 12.5rem;
+            max-width: 95vw;
+            height: auto;
+            border-radius: 5px;
+            border: 1px solid #eaeaea;
+            color: #000;
+            transition: border 0.2s ease 0s, color 0.2s ease 0s;
+          }
+
+          .wrapper.hover {
+            border-color: #666;
+          }
+
+          .wrapper.disabled {
+            background-color: #fafafa;
+            border-color: #eaeaea;
+            cursor: not-allowed;
+          }
+
+          textarea {
+            background-color: transparent;
+            box-shadow: none;
+            display: block;
+            font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", sans-serif;
+            font-size: 0.875rem;
+            width: 100%;
+            height: 100%;
+            min-height: 100px;
+            resize: none;
+            border: none;
+            outline: none;
+            padding: 8pt;
+          }
+
+          .disabled > textarea {
+            cursor: not-allowed;
+          }
+
+          textarea:-webkit-autofill,
+          textarea:-webkit-autofill:hover,
+          textarea:-webkit-autofill:active,
+          textarea:-webkit-autofill:focus {
+            -webkit-box-shadow: 0 0 0 30px #fff inset !important;
+          }
+        </style></div></div>"
 `;
diff --git a/components/use-toasts/__tests__/__snapshots__/index.test.tsx.snap b/components/use-toasts/__tests__/__snapshots__/index.test.tsx.snap
index 03acd43..7f82aa6 100644
--- a/components/use-toasts/__tests__/__snapshots__/index.test.tsx.snap
+++ b/components/use-toasts/__tests__/__snapshots__/index.test.tsx.snap
@@ -35,7 +35,8 @@ exports[`UseToast should render different actions 1`] = `
             --zeit-ui-button-bg: #000;
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
             color: #000;
             --zeit-ui-button-color: #000;
             background-color: #fff;
@@ -96,7 +97,8 @@ exports[`UseToast should render different actions 1`] = `
             --zeit-ui-button-bg: #fff;
           }
 
-          .btn:hover {
+          .btn:hover,
+          .btn:focus {
             color: #000;
             --zeit-ui-button-color: #000;
             background-color: #fff;